summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonstantin Alekseev <mail@kalekseev.com>2018-12-06 18:08:01 +0300
committerTim Graham <timograham@gmail.com>2018-12-20 22:17:21 -0500
commit08f78a4fc8e7882f9fcad19aa6d497f749a9d261 (patch)
tree2c1c9e375434aff5dca7f3c046b4d607974933de
parent60398f0e3979e25881f80db79155953b65925ab1 (diff)
[2.1.x] Fixed #30015 -- Ensured request body is properly consumed for keep-alive connections.
Backport of b514dc14f4e1c364341f5931b354e83ef15ee12d and bbe28fa07658f00786dc1d91ee281b4daac22d07 from master.
-rw-r--r--django/core/servers/basehttp.py28
-rw-r--r--docs/releases/2.1.5.txt4
-rw-r--r--tests/servers/tests.py17
-rw-r--r--tests/servers/urls.py1
-rw-r--r--tests/servers/views.py6
5 files changed, 51 insertions, 5 deletions
diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py
index 8a36b67eef..3e8f176bc6 100644
--- a/django/core/servers/basehttp.py
+++ b/django/core/servers/basehttp.py
@@ -14,6 +14,7 @@ import sys
from wsgiref import simple_server
from django.core.exceptions import ImproperlyConfigured
+from django.core.handlers.wsgi import LimitedStream
from django.core.wsgi import get_wsgi_application
from django.utils.module_loading import import_string
@@ -80,18 +81,35 @@ class ThreadedWSGIServer(socketserver.ThreadingMixIn, WSGIServer):
class ServerHandler(simple_server.ServerHandler):
http_version = '1.1'
+ def __init__(self, stdin, stdout, stderr, environ, **kwargs):
+ """
+ Use a LimitedStream so that unread request data will be ignored at
+ the end of the request. WSGIRequest uses a LimitedStream but it
+ shouldn't discard the data since the upstream servers usually do this.
+ This fix applies only for testserver/runserver.
+ """
+ try:
+ content_length = int(environ.get('CONTENT_LENGTH'))
+ except (ValueError, TypeError):
+ content_length = 0
+ super().__init__(LimitedStream(stdin, content_length), stdout, stderr, environ, **kwargs)
+
def cleanup_headers(self):
super().cleanup_headers()
- # HTTP/1.1 requires us to support persistent connections, so
- # explicitly send close if we do not know the content length to
- # prevent clients from reusing the connection.
+ # HTTP/1.1 requires support for persistent connections. Send 'close' if
+ # the content length is unknown to prevent clients from reusing the
+ # connection.
if 'Content-Length' not in self.headers:
self.headers['Connection'] = 'close'
- # Mark the connection for closing if we set it as such above or
- # if the application sent the header.
+ # Mark the connection for closing if it's set as such above or if the
+ # application sent the header.
if self.headers.get('Connection') == 'close':
self.request_handler.close_connection = True
+ def close(self):
+ self.get_stdin()._read_limited()
+ super().close()
+
def handle_error(self):
# Ignore broken pipe errors, otherwise pass on
if not is_broken_pipe_error():
diff --git a/docs/releases/2.1.5.txt b/docs/releases/2.1.5.txt
index f5306192a4..73850e01b5 100644
--- a/docs/releases/2.1.5.txt
+++ b/docs/releases/2.1.5.txt
@@ -18,3 +18,7 @@ Bugfixes
* Prevented SQLite schema alterations while foreign key checks are enabled to
avoid the possibility of schema corruption (:ticket:`30023`).
+
+* Fixed a regression in Django 2.1.4 (which enabled keep-alive connections)
+ where request body data isn't properly consumed for such connections
+ (:ticket:`30015`).
diff --git a/tests/servers/tests.py b/tests/servers/tests.py
index e38cb5eb07..5917e30d24 100644
--- a/tests/servers/tests.py
+++ b/tests/servers/tests.py
@@ -111,6 +111,23 @@ class LiveServerViews(LiveServerBase):
finally:
conn.close()
+ def test_keep_alive_connection_clears_previous_request_data(self):
+ conn = HTTPConnection(LiveServerViews.server_thread.host, LiveServerViews.server_thread.port)
+ try:
+ conn.request('POST', '/method_view/', b'{}', headers={"Connection": "keep-alive"})
+ response = conn.getresponse()
+ self.assertFalse(response.will_close)
+ self.assertEqual(response.status, 200)
+ self.assertEqual(response.read(), b'POST')
+
+ conn.request('POST', '/method_view/', b'{}', headers={"Connection": "close"})
+ response = conn.getresponse()
+ self.assertFalse(response.will_close)
+ self.assertEqual(response.status, 200)
+ self.assertEqual(response.read(), b'POST')
+ finally:
+ conn.close()
+
def test_404(self):
with self.assertRaises(HTTPError) as err:
self.urlopen('/')
diff --git a/tests/servers/urls.py b/tests/servers/urls.py
index 9017161808..0a8a2984aa 100644
--- a/tests/servers/urls.py
+++ b/tests/servers/urls.py
@@ -11,4 +11,5 @@ urlpatterns = [
url(r'^subview_calling_view/$', views.subview_calling_view),
url(r'^subview/$', views.subview),
url(r'^check_model_instance_from_subview/$', views.check_model_instance_from_subview),
+ url(r'^method_view/$', views.method_view),
]
diff --git a/tests/servers/views.py b/tests/servers/views.py
index 078be67f46..1db56f44a3 100644
--- a/tests/servers/views.py
+++ b/tests/servers/views.py
@@ -1,6 +1,7 @@
from urllib.request import urlopen
from django.http import HttpResponse, StreamingHttpResponse
+from django.views.decorators.csrf import csrf_exempt
from .models import Person
@@ -42,3 +43,8 @@ def check_model_instance_from_subview(request):
pass
with urlopen(request.GET['url'] + '/model_view/') as response:
return HttpResponse('subview calling view: {}'.format(response.read().decode()))
+
+
+@csrf_exempt
+def method_view(request):
+ return HttpResponse(request.method)