This commit is contained in:
Ryan P Kilby 2018-01-31 15:55:10 +00:00 committed by GitHub
commit cfe1a3ff02
4 changed files with 116 additions and 29 deletions

View File

@ -130,6 +130,9 @@ As REST framework's `Request` extends Django's `HttpRequest`, all the other stan
Note that due to implementation reasons the `Request` class does not inherit from `HttpRequest` class, but instead extends the class using composition. Note that due to implementation reasons the `Request` class does not inherit from `HttpRequest` class, but instead extends the class using composition.
# Accessing the HttpRequest
The underlying `HttpRequest` can be accessed through the `.http_request` attribute. While direct manipulation of the `HttpRequest` is discouraged, there are some advanced use cases that may require it. For example, one view may delegate request handling to a secondary view function. In this case, it is necessary to pass the original `HttpRequest` to the delegated view instead of the DRF `Request` object. Be aware that duplicate processing of the `HttpRequest` may have have unintended side effects. For example, if the request stream has already been consumed, it may not be accessible for a second read and will raise an exception.
[cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion [cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion
[parsers documentation]: parsers.md [parsers documentation]: parsers.md

View File

@ -121,7 +121,7 @@ class SessionAuthentication(BaseAuthentication):
""" """
# Get the session-based user from the underlying HttpRequest object # Get the session-based user from the underlying HttpRequest object
user = getattr(request._request, 'user', None) user = getattr(request.http_request, 'user', None)
# Unauthenticated, CSRF validation not required # Unauthenticated, CSRF validation not required
if not user or not user.is_active: if not user or not user.is_active:

View File

@ -11,6 +11,7 @@ The wrapped request then offers a richer API, in particular :
from __future__ import unicode_literals from __future__ import unicode_literals
import sys import sys
import warnings
from contextlib import contextmanager from contextlib import contextmanager
from django.conf import settings from django.conf import settings
@ -97,7 +98,7 @@ def clone_request(request, method):
Internal helper method to clone a request, replacing with a different Internal helper method to clone a request, replacing with a different
HTTP method. Used for checking permissions against other methods. HTTP method. Used for checking permissions against other methods.
""" """
ret = Request(request=request._request, ret = Request(request=request.http_request,
parsers=request.parsers, parsers=request.parsers,
authenticators=request.authenticators, authenticators=request.authenticators,
negotiator=request.negotiator, negotiator=request.negotiator,
@ -159,7 +160,7 @@ class Request(object):
.format(request.__class__.__module__, request.__class__.__name__) .format(request.__class__.__module__, request.__class__.__name__)
) )
self._request = request self.http_request = request
self.parsers = parsers or () self.parsers = parsers or ()
self.authenticators = authenticators or () self.authenticators = authenticators or ()
self.negotiator = negotiator or self._default_negotiator() self.negotiator = negotiator or self._default_negotiator()
@ -181,12 +182,28 @@ class Request(object):
forced_auth = ForcedAuthentication(force_user, force_token) forced_auth = ForcedAuthentication(force_user, force_token)
self.authenticators = (forced_auth,) self.authenticators = (forced_auth,)
@property
def _request(self):
warnings.warn(
"`_request` has been deprecated in favor of `http_request`, and will be removed in 3.10",
PendingDeprecationWarning, stacklevel=2
)
return self.http_request
@_request.setter
def _request(self, value):
warnings.warn(
"`_request` has been deprecated in favor of `http_request`, and will be removed in 3.10",
PendingDeprecationWarning, stacklevel=2
)
self.http_request = value
def _default_negotiator(self): def _default_negotiator(self):
return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS() return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS()
@property @property
def content_type(self): def content_type(self):
meta = self._request.META meta = self.http_request.META
return meta.get('CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', '')) return meta.get('CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', ''))
@property @property
@ -203,7 +220,7 @@ class Request(object):
""" """
More semantically correct name for request.GET. More semantically correct name for request.GET.
""" """
return self._request.GET return self.http_request.GET
@property @property
def data(self): def data(self):
@ -233,7 +250,7 @@ class Request(object):
instance, ensuring that it is available to any middleware in the stack. instance, ensuring that it is available to any middleware in the stack.
""" """
self._user = value self._user = value
self._request.user = value self.http_request.user = value
@property @property
def auth(self): def auth(self):
@ -253,7 +270,7 @@ class Request(object):
request, such as an authentication token. request, such as an authentication token.
""" """
self._auth = value self._auth = value
self._request.auth = value self.http_request.auth = value
@property @property
def successful_authenticator(self): def successful_authenticator(self):
@ -278,16 +295,17 @@ class Request(object):
else: else:
self._full_data = self._data self._full_data = self._data
# copy data & files refs to the underlying request so that closable # if a form media type, copy data & files refs to the underlying
# objects are handled appropriately. # http request so that closable objects are handled appropriately.
self._request._post = self.POST if is_form_media_type(self.content_type):
self._request._files = self.FILES self.http_request._post = self.POST
self.http_request._files = self.FILES
def _load_stream(self): def _load_stream(self):
""" """
Return the content body of the request, as a stream. Return the content body of the request, as a stream.
""" """
meta = self._request.META meta = self.http_request.META
try: try:
content_length = int( content_length = int(
meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0)) meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0))
@ -297,8 +315,8 @@ class Request(object):
if content_length == 0: if content_length == 0:
self._stream = None self._stream = None
elif not self._request._read_started: elif not self.http_request._read_started:
self._stream = self._request self._stream = self.http_request
else: else:
self._stream = six.BytesIO(self.body) self._stream = six.BytesIO(self.body)
@ -322,18 +340,18 @@ class Request(object):
try: try:
stream = self.stream stream = self.stream
except RawPostDataException: except RawPostDataException:
if not hasattr(self._request, '_post'): if not hasattr(self.http_request, '_post'):
raise raise
# If request.POST has been accessed in middleware, and a method='POST' # If request.POST has been accessed in middleware, and a method='POST'
# request was made with 'multipart/form-data', then the request stream # request was made with 'multipart/form-data', then the request stream
# will already have been exhausted. # will already have been exhausted.
if self._supports_form_parsing(): if self._supports_form_parsing():
return (self._request.POST, self._request.FILES) return (self.http_request.POST, self.http_request.FILES)
stream = None stream = None
if stream is None or media_type is None: if stream is None or media_type is None:
if media_type and is_form_media_type(media_type): if media_type and is_form_media_type(media_type):
empty_data = QueryDict('', encoding=self._request._encoding) empty_data = QueryDict('', encoding=self.http_request._encoding)
else: else:
empty_data = {} empty_data = {}
empty_files = MultiValueDict() empty_files = MultiValueDict()
@ -351,7 +369,7 @@ class Request(object):
# re-raise. Ensures we don't simply repeat the error when # re-raise. Ensures we don't simply repeat the error when
# attempting to render the browsable renderer response, or when # attempting to render the browsable renderer response, or when
# logging the request or similar. # logging the request or similar.
self._data = QueryDict('', encoding=self._request._encoding) self._data = QueryDict('', encoding=self.http_request._encoding)
self._files = MultiValueDict() self._files = MultiValueDict()
self._full_data = self._data self._full_data = self._data
raise raise
@ -407,7 +425,7 @@ class Request(object):
to proxy it to the underlying HttpRequest object. to proxy it to the underlying HttpRequest object.
""" """
try: try:
return getattr(self._request, attr) return getattr(self.http_request, attr)
except AttributeError: except AttributeError:
return self.__getattribute__(attr) return self.__getattribute__(attr)
@ -425,7 +443,7 @@ class Request(object):
self._load_data_and_files() self._load_data_and_files()
if is_form_media_type(self.content_type): if is_form_media_type(self.content_type):
return self._data return self._data
return QueryDict('', encoding=self._request._encoding) return QueryDict('', encoding=self.http_request._encoding)
@property @property
def FILES(self): def FILES(self):
@ -446,4 +464,4 @@ class Request(object):
def force_plaintext_errors(self, value): def force_plaintext_errors(self, value):
# Hack to allow our exception handler to force choice of # Hack to allow our exception handler to force choice of
# plaintext or html error responses. # plaintext or html error responses.
self._request.is_ajax = lambda: value self.http_request.is_ajax = lambda: value

View File

@ -13,6 +13,7 @@ from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.http.request import RawPostDataException
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.utils import six from django.utils import six
@ -137,6 +138,11 @@ class MockView(APIView):
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class EchoView(APIView):
def post(self, request):
return Response(status=status.HTTP_200_OK, data=request.data)
class FileUploadView(APIView): class FileUploadView(APIView):
def post(self, request): def post(self, request):
filenames = [file.temporary_file_path() for file in request.FILES.values()] filenames = [file.temporary_file_path() for file in request.FILES.values()]
@ -149,6 +155,7 @@ class FileUploadView(APIView):
urlpatterns = [ urlpatterns = [
url(r'^$', MockView.as_view()), url(r'^$', MockView.as_view()),
url(r'^echo/$', EchoView.as_view()),
url(r'^upload/$', FileUploadView.as_view()) url(r'^upload/$', FileUploadView.as_view())
] ]
@ -271,24 +278,83 @@ class TestSecure(TestCase):
assert request.scheme == 'https' assert request.scheme == 'https'
class TestWSGIRequestProxy(TestCase): class TestHttpRequest(TestCase):
def test_attribute_access(self): def test_attribute_access_proxy(self):
wsgi_request = factory.get('/') http_request = factory.get('/')
request = Request(wsgi_request) request = Request(http_request)
inner_sentinel = object() inner_sentinel = object()
wsgi_request.inner_property = inner_sentinel http_request.inner_property = inner_sentinel
assert request.inner_property is inner_sentinel assert request.inner_property is inner_sentinel
outer_sentinel = object() outer_sentinel = object()
request.inner_property = outer_sentinel request.inner_property = outer_sentinel
assert request.inner_property is outer_sentinel assert request.inner_property is outer_sentinel
def test_exception(self): def test_exception_proxy(self):
# ensure the exception message is not for the underlying WSGIRequest # ensure the exception message is not for the underlying WSGIRequest
wsgi_request = factory.get('/') http_request = factory.get('/')
request = Request(wsgi_request) request = Request(http_request)
message = "'Request' object has no attribute 'inner_property'" message = "'Request' object has no attribute 'inner_property'"
with self.assertRaisesMessage(AttributeError, message): with self.assertRaisesMessage(AttributeError, message):
request.inner_property request.inner_property
def test_request_deprecation(self):
with pytest.warns(PendingDeprecationWarning) as record:
Request(factory.get('/'))._request
assert len(record) == 1
assert str(record[0].message) == (
"`_request` has been deprecated in favor of "
"`http_request`, and will be removed in 3.10"
)
with pytest.warns(PendingDeprecationWarning) as record:
Request(factory.get('/'))._request = None
assert len(record) == 1
assert str(record[0].message) == (
"`_request` has been deprecated in favor of "
"`http_request`, and will be removed in 3.10"
)
@override_settings(ROOT_URLCONF='tests.test_request')
def test_duplicate_request_stream_parsing_exception(self):
"""
Check assumption that duplicate stream parsing will result in a
`RawPostDataException` being raised.
"""
response = APIClient().post('/echo/', data={'a': 'b'}, format='json')
request = response.renderer_context['request']
# ensure that request stream was consumed by json parser
assert request.content_type.startswith('application/json')
assert response.data == {'a': 'b'}
# pass same HttpRequest to view, stream already consumed
with pytest.raises(RawPostDataException):
EchoView.as_view()(request.http_request)
@override_settings(ROOT_URLCONF='tests.test_request')
def test_duplicate_request_form_data_access(self):
"""
Form data is copied to the underlying django request for middleware
and file closing reasons. Duplicate processing of a request with form
data is 'safe' in so far as accessing `request.POST` does not trigger
the duplicate stream parse exception.
"""
response = APIClient().post('/echo/', data={'a': 'b'})
request = response.renderer_context['request']
# ensure that request stream was consumed by form parser
assert request.content_type.startswith('multipart/form-data')
assert response.data == {'a': ['b']}
# pass same HttpRequest to view, form data set on underlying request
response = EchoView.as_view()(request.http_request)
request = response.renderer_context['request']
# ensure that request stream was consumed by form parser
assert request.content_type.startswith('multipart/form-data')
assert response.data == {'a': ['b']}