Fixed #19519 -- Fired request_finished in the WSGI iterable's close().
This commit is contained in:
parent
a53c474026
commit
acc5396e6d
|
@ -253,8 +253,8 @@ class WSGIHandler(base.BaseHandler):
|
||||||
response = http.HttpResponseBadRequest()
|
response = http.HttpResponseBadRequest()
|
||||||
else:
|
else:
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
finally:
|
|
||||||
signals.request_finished.send(sender=self.__class__)
|
response._handler_class = self.__class__
|
||||||
|
|
||||||
try:
|
try:
|
||||||
status_text = STATUS_CODE_TEXT[response.status_code]
|
status_text = STATUS_CODE_TEXT[response.status_code]
|
||||||
|
|
|
@ -10,6 +10,7 @@ except ImportError:
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core import signals
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.http.cookie import SimpleCookie
|
from django.http.cookie import SimpleCookie
|
||||||
|
@ -40,6 +41,9 @@ class HttpResponseBase(six.Iterator):
|
||||||
self._headers = {}
|
self._headers = {}
|
||||||
self._charset = settings.DEFAULT_CHARSET
|
self._charset = settings.DEFAULT_CHARSET
|
||||||
self._closable_objects = []
|
self._closable_objects = []
|
||||||
|
# This parameter is set by the handler. It's necessary to preserve the
|
||||||
|
# historical behavior of request_finished.
|
||||||
|
self._handler_class = None
|
||||||
if mimetype:
|
if mimetype:
|
||||||
warnings.warn("Using mimetype keyword argument is deprecated, use"
|
warnings.warn("Using mimetype keyword argument is deprecated, use"
|
||||||
" content_type instead",
|
" content_type instead",
|
||||||
|
@ -226,7 +230,11 @@ class HttpResponseBase(six.Iterator):
|
||||||
# See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html
|
# See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html
|
||||||
def close(self):
|
def close(self):
|
||||||
for closable in self._closable_objects:
|
for closable in self._closable_objects:
|
||||||
closable.close()
|
try:
|
||||||
|
closable.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
signals.request_finished.send(sender=self._handler_class)
|
||||||
|
|
||||||
def write(self, content):
|
def write(self, content):
|
||||||
raise Exception("This %s instance is not writable" % self.__class__.__name__)
|
raise Exception("This %s instance is not writable" % self.__class__.__name__)
|
||||||
|
|
|
@ -26,7 +26,6 @@ from django.utils.http import urlencode
|
||||||
from django.utils.importlib import import_module
|
from django.utils.importlib import import_module
|
||||||
from django.utils.itercompat import is_iterable
|
from django.utils.itercompat import is_iterable
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.db import close_connection
|
|
||||||
from django.test.utils import ContextList
|
from django.test.utils import ContextList
|
||||||
|
|
||||||
__all__ = ('Client', 'RequestFactory', 'encode_file', 'encode_multipart')
|
__all__ = ('Client', 'RequestFactory', 'encode_file', 'encode_multipart')
|
||||||
|
@ -72,6 +71,14 @@ class FakePayload(object):
|
||||||
self.__len += len(content)
|
self.__len += len(content)
|
||||||
|
|
||||||
|
|
||||||
|
def closing_iterator_wrapper(iterable, close):
|
||||||
|
try:
|
||||||
|
for item in iterable:
|
||||||
|
yield item
|
||||||
|
finally:
|
||||||
|
close()
|
||||||
|
|
||||||
|
|
||||||
class ClientHandler(BaseHandler):
|
class ClientHandler(BaseHandler):
|
||||||
"""
|
"""
|
||||||
A HTTP Handler that can be used for testing purposes.
|
A HTTP Handler that can be used for testing purposes.
|
||||||
|
@ -92,18 +99,20 @@ class ClientHandler(BaseHandler):
|
||||||
self.load_middleware()
|
self.load_middleware()
|
||||||
|
|
||||||
signals.request_started.send(sender=self.__class__)
|
signals.request_started.send(sender=self.__class__)
|
||||||
try:
|
request = WSGIRequest(environ)
|
||||||
request = WSGIRequest(environ)
|
# sneaky little hack so that we can easily get round
|
||||||
# sneaky little hack so that we can easily get round
|
# CsrfViewMiddleware. This makes life easier, and is probably
|
||||||
# CsrfViewMiddleware. This makes life easier, and is probably
|
# required for backwards compatibility with external tests against
|
||||||
# required for backwards compatibility with external tests against
|
# admin views.
|
||||||
# admin views.
|
request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
|
||||||
request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
|
response = self.get_response(request)
|
||||||
response = self.get_response(request)
|
# We're emulating a WSGI server; we must call the close method
|
||||||
finally:
|
# on completion.
|
||||||
signals.request_finished.disconnect(close_connection)
|
if response.streaming:
|
||||||
signals.request_finished.send(sender=self.__class__)
|
response.streaming_content = closing_iterator_wrapper(
|
||||||
signals.request_finished.connect(close_connection)
|
response.streaming_content, response.close)
|
||||||
|
else:
|
||||||
|
response.close()
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ from xml.dom.minidom import parseString, Node
|
||||||
|
|
||||||
from django.conf import settings, UserSettingsHolder
|
from django.conf import settings, UserSettingsHolder
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
|
from django.core.signals import request_finished
|
||||||
|
from django.db import close_connection
|
||||||
from django.test.signals import template_rendered, setting_changed
|
from django.test.signals import template_rendered, setting_changed
|
||||||
from django.template import Template, loader, TemplateDoesNotExist
|
from django.template import Template, loader, TemplateDoesNotExist
|
||||||
from django.template.loaders import cached
|
from django.template.loaders import cached
|
||||||
|
@ -68,8 +70,10 @@ def setup_test_environment():
|
||||||
"""Perform any global pre-test setup. This involves:
|
"""Perform any global pre-test setup. This involves:
|
||||||
|
|
||||||
- Installing the instrumented test renderer
|
- Installing the instrumented test renderer
|
||||||
- Set the email backend to the locmem email backend.
|
- Setting the email backend to the locmem email backend.
|
||||||
- Setting the active locale to match the LANGUAGE_CODE setting.
|
- Setting the active locale to match the LANGUAGE_CODE setting.
|
||||||
|
- Disconnecting the request_finished signal to avoid closing
|
||||||
|
the database connection within tests.
|
||||||
"""
|
"""
|
||||||
Template.original_render = Template._render
|
Template.original_render = Template._render
|
||||||
Template._render = instrumented_test_render
|
Template._render = instrumented_test_render
|
||||||
|
@ -81,6 +85,8 @@ def setup_test_environment():
|
||||||
|
|
||||||
deactivate()
|
deactivate()
|
||||||
|
|
||||||
|
request_finished.disconnect(close_connection)
|
||||||
|
|
||||||
|
|
||||||
def teardown_test_environment():
|
def teardown_test_environment():
|
||||||
"""Perform any global post-test teardown. This involves:
|
"""Perform any global post-test teardown. This involves:
|
||||||
|
|
|
@ -790,6 +790,8 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in
|
||||||
:class:`~django.template.response.SimpleTemplateResponse`, and the
|
:class:`~django.template.response.SimpleTemplateResponse`, and the
|
||||||
``render`` method must itself return a valid response object.
|
``render`` method must itself return a valid response object.
|
||||||
|
|
||||||
|
.. _httpresponse-streaming:
|
||||||
|
|
||||||
StreamingHttpResponse objects
|
StreamingHttpResponse objects
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|
|
@ -448,6 +448,18 @@ request_finished
|
||||||
|
|
||||||
Sent when Django finishes processing an HTTP request.
|
Sent when Django finishes processing an HTTP request.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When a view returns a :ref:`streaming response <httpresponse-streaming>`,
|
||||||
|
this signal is sent only after the entire response is consumed by the
|
||||||
|
client (strictly speaking, by the WSGI gateway).
|
||||||
|
|
||||||
|
.. versionchanged:: 1.5
|
||||||
|
|
||||||
|
Before Django 1.5, this signal was fired before sending the content to the
|
||||||
|
client. In order to accomodate streaming responses, it is now fired after
|
||||||
|
sending the content.
|
||||||
|
|
||||||
Arguments sent with this signal:
|
Arguments sent with this signal:
|
||||||
|
|
||||||
``sender``
|
``sender``
|
||||||
|
|
|
@ -411,6 +411,19 @@ attribute. Developers wishing to access the raw POST data for these cases,
|
||||||
should use the :attr:`request.body <django.http.HttpRequest.body>` attribute
|
should use the :attr:`request.body <django.http.HttpRequest.body>` attribute
|
||||||
instead.
|
instead.
|
||||||
|
|
||||||
|
:data:`~django.core.signals.request_finished` signal
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Django used to send the :data:`~django.core.signals.request_finished` signal
|
||||||
|
as soon as the view function returned a response. This interacted badly with
|
||||||
|
:ref:`streaming responses <httpresponse-streaming>` that delay content
|
||||||
|
generation.
|
||||||
|
|
||||||
|
This signal is now sent after the content is fully consumed by the WSGI
|
||||||
|
gateway. This might be backwards incompatible if you rely on the signal being
|
||||||
|
fired before sending the response content to the client. If you do, you should
|
||||||
|
consider using a middleware instead.
|
||||||
|
|
||||||
OPTIONS, PUT and DELETE requests in the test client
|
OPTIONS, PUT and DELETE requests in the test client
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from django.core.handlers.wsgi import WSGIHandler
|
from django.core.handlers.wsgi import WSGIHandler
|
||||||
from django.test import RequestFactory
|
from django.core import signals
|
||||||
|
from django.test import RequestFactory, TestCase
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils import unittest
|
|
||||||
|
|
||||||
class HandlerTests(unittest.TestCase):
|
|
||||||
|
class HandlerTests(TestCase):
|
||||||
|
|
||||||
# Mangle settings so the handler will fail
|
# Mangle settings so the handler will fail
|
||||||
@override_settings(MIDDLEWARE_CLASSES=42)
|
@override_settings(MIDDLEWARE_CLASSES=42)
|
||||||
|
@ -27,3 +28,34 @@ class HandlerTests(unittest.TestCase):
|
||||||
handler = WSGIHandler()
|
handler = WSGIHandler()
|
||||||
response = handler(environ, lambda *a, **k: None)
|
response = handler(environ, lambda *a, **k: None)
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
|
||||||
|
class SignalsTests(TestCase):
|
||||||
|
urls = 'regressiontests.handlers.urls'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.signals = []
|
||||||
|
signals.request_started.connect(self.register_started)
|
||||||
|
signals.request_finished.connect(self.register_finished)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
signals.request_started.disconnect(self.register_started)
|
||||||
|
signals.request_finished.disconnect(self.register_finished)
|
||||||
|
|
||||||
|
def register_started(self, **kwargs):
|
||||||
|
self.signals.append('started')
|
||||||
|
|
||||||
|
def register_finished(self, **kwargs):
|
||||||
|
self.signals.append('finished')
|
||||||
|
|
||||||
|
def test_request_signals(self):
|
||||||
|
response = self.client.get('/regular/')
|
||||||
|
self.assertEqual(self.signals, ['started', 'finished'])
|
||||||
|
self.assertEqual(response.content, b"regular content")
|
||||||
|
|
||||||
|
def test_request_signals_streaming_response(self):
|
||||||
|
response = self.client.get('/streaming/')
|
||||||
|
self.assertEqual(self.signals, ['started'])
|
||||||
|
# Avoid self.assertContains, because it explicitly calls response.close()
|
||||||
|
self.assertEqual(b''.join(response.streaming_content), b"streaming content")
|
||||||
|
self.assertEqual(self.signals, ['started', 'finished'])
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf.urls import patterns, url
|
||||||
|
from django.http import HttpResponse, StreamingHttpResponse
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
url(r'^regular/$', lambda request: HttpResponse(b"regular content")),
|
||||||
|
url(r'^streaming/$', lambda request: StreamingHttpResponse([b"streaming", b" ", b"content"])),
|
||||||
|
)
|
Loading…
Reference in New Issue