mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +00:00 
			
		
		
		
	Fixed #19519 -- Fired request_finished in the WSGI iterable's close().
This commit is contained in:
		| @@ -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']) | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								tests/regressiontests/handlers/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								tests/regressiontests/handlers/urls.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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"])), | ||||||
|  | ) | ||||||
		Reference in New Issue
	
	Block a user