Skip to content

Commit

Permalink
[1.5.x] Fixed #19519 -- Fired request_finished in the WSGI iterable's…
Browse files Browse the repository at this point in the history
… close().

Backport of acc5396.
  • Loading branch information
aaugustin committed Dec 31, 2012
1 parent ac72782 commit fd1279a
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 20 deletions.
4 changes: 2 additions & 2 deletions django/core/handlers/wsgi.py
Expand Up @@ -253,8 +253,8 @@ def __call__(self, environ, start_response):
response = http.HttpResponseBadRequest()
else:
response = self.get_response(request)
finally:
signals.request_finished.send(sender=self.__class__)

response._handler_class = self.__class__

try:
status_text = STATUS_CODE_TEXT[response.status_code]
Expand Down
10 changes: 9 additions & 1 deletion django/http/response.py
Expand Up @@ -10,6 +10,7 @@
from urlparse import urlparse

from django.conf import settings
from django.core import signals
from django.core import signing
from django.core.exceptions import SuspiciousOperation
from django.http.cookie import SimpleCookie
Expand Down Expand Up @@ -40,6 +41,9 @@ def __init__(self, content_type=None, status=None, mimetype=None):
self._headers = {}
self._charset = settings.DEFAULT_CHARSET
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:
warnings.warn("Using mimetype keyword argument is deprecated, use"
" content_type instead", PendingDeprecationWarning)
Expand Down Expand Up @@ -225,7 +229,11 @@ def __next__(self):
# See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html
def close(self):
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):
raise Exception("This %s instance is not writable" % self.__class__.__name__)
Expand Down
35 changes: 22 additions & 13 deletions django/test/client.py
Expand Up @@ -26,7 +26,6 @@
from django.utils.importlib import import_module
from django.utils.itercompat import is_iterable
from django.utils import six
from django.db import close_connection
from django.test.utils import ContextList

__all__ = ('Client', 'RequestFactory', 'encode_file', 'encode_multipart')
Expand Down Expand Up @@ -72,6 +71,14 @@ def write(self, content):
self.__len += len(content)


def closing_iterator_wrapper(iterable, close):
try:
for item in iterable:
yield item
finally:
close()


class ClientHandler(BaseHandler):
"""
A HTTP Handler that can be used for testing purposes.
Expand All @@ -92,18 +99,20 @@ def __call__(self, environ):
self.load_middleware()

signals.request_started.send(sender=self.__class__)
try:
request = WSGIRequest(environ)
# sneaky little hack so that we can easily get round
# CsrfViewMiddleware. This makes life easier, and is probably
# required for backwards compatibility with external tests against
# admin views.
request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
response = self.get_response(request)
finally:
signals.request_finished.disconnect(close_connection)
signals.request_finished.send(sender=self.__class__)
signals.request_finished.connect(close_connection)
request = WSGIRequest(environ)
# sneaky little hack so that we can easily get round
# CsrfViewMiddleware. This makes life easier, and is probably
# required for backwards compatibility with external tests against
# admin views.
request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
response = self.get_response(request)
# We're emulating a WSGI server; we must call the close method
# on completion.
if response.streaming:
response.streaming_content = closing_iterator_wrapper(
response.streaming_content, response.close)
else:
response.close()

return response

Expand Down
8 changes: 7 additions & 1 deletion django/test/utils.py
Expand Up @@ -4,6 +4,8 @@

from django.conf import settings, UserSettingsHolder
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.template import Template, loader, TemplateDoesNotExist
from django.template.loaders import cached
Expand Down Expand Up @@ -68,8 +70,10 @@ def setup_test_environment():
"""Perform any global pre-test setup. This involves:
- 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.
- Disconnecting the request_finished signal to avoid closing
the database connection within tests.
"""
Template.original_render = Template._render
Template._render = instrumented_test_render
Expand All @@ -81,6 +85,8 @@ def setup_test_environment():

deactivate()

request_finished.disconnect(close_connection)


def teardown_test_environment():
"""Perform any global post-test teardown. This involves:
Expand Down
2 changes: 2 additions & 0 deletions docs/ref/request-response.txt
Expand Up @@ -804,6 +804,8 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in
:class:`~django.template.response.SimpleTemplateResponse`, and the
``render`` method must itself return a valid response object.

.. _httpresponse-streaming:

StreamingHttpResponse objects
=============================

Expand Down
12 changes: 12 additions & 0 deletions docs/ref/signals.txt
Expand Up @@ -448,6 +448,18 @@ request_finished

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:

``sender``
Expand Down
13 changes: 13 additions & 0 deletions docs/releases/1.5.txt
Expand Up @@ -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
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
38 changes: 35 additions & 3 deletions tests/regressiontests/handlers/tests.py
@@ -1,10 +1,11 @@
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.utils import six
from django.utils import unittest

class HandlerTests(unittest.TestCase):

class HandlerTests(TestCase):

# Mangle settings so the handler will fail
@override_settings(MIDDLEWARE_CLASSES=42)
Expand All @@ -27,3 +28,34 @@ def test_bad_path_info(self):
handler = WSGIHandler()
response = handler(environ, lambda *a, **k: None)
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'])
9 changes: 9 additions & 0 deletions tests/regressiontests/handlers/urls.py
@@ -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"])),
)

0 comments on commit fd1279a

Please sign in to comment.