summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Shepelev <temotor@gmail.com>2017-01-05 07:25:01 +0300
committerSergey Shepelev <temotor@gmail.com>2017-01-05 09:06:15 +0300
commit7595a5a4b789ce9492f88c96a412cdf1e47e60cc (patch)
tree5e2e235f9a871e9bb54792553d4080f623574ecf
parented79125c652d7ea02cb79a84c1292fdc19932a94 (diff)
downloadeventlet-7595a5a4b789ce9492f88c96a412cdf1e47e60cc.tar.gz
python3.6: http.client.request support chunked_encoding
https://bugs.python.org/issue12319
-rw-r--r--eventlet/green/http/client.py144
-rw-r--r--tests/green_http_test.py12
2 files changed, 142 insertions, 14 deletions
diff --git a/eventlet/green/http/client.py b/eventlet/green/http/client.py
index c7d3bda..07ebccf 100644
--- a/eventlet/green/http/client.py
+++ b/eventlet/green/http/client.py
@@ -849,6 +849,44 @@ class HTTPConnection:
auto_open = 1
debuglevel = 0
+ @staticmethod
+ def _is_textIO(stream):
+ """Test whether a file-like object is a text or a binary stream.
+ """
+ return isinstance(stream, io.TextIOBase)
+
+ @staticmethod
+ def _get_content_length(body, method):
+ """Get the content-length based on the body.
+
+ If the body is None, we set Content-Length: 0 for methods that expect
+ a body (RFC 7230, Section 3.3.2). We also set the Content-Length for
+ any method if the body is a str or bytes-like object and not a file.
+ """
+ if body is None:
+ # do an explicit check for not None here to distinguish
+ # between unset and set but empty
+ if method.upper() in _METHODS_EXPECTING_BODY:
+ return 0
+ else:
+ return None
+
+ if hasattr(body, 'read'):
+ # file-like object.
+ return None
+
+ try:
+ # does it implement the buffer protocol (bytes, bytearray, array)?
+ mv = memoryview(body)
+ return mv.nbytes
+ except TypeError:
+ pass
+
+ if isinstance(body, str):
+ return len(body)
+
+ return None
+
def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
source_address=None):
self.timeout = timeout
@@ -1024,7 +1062,22 @@ class HTTPConnection:
"""
self._buffer.append(s)
- def _send_output(self, message_body=None):
+ def _read_readable(self, readable):
+ blocksize = 8192
+ if self.debuglevel > 0:
+ print("sendIng a read()able")
+ encode = self._is_textIO(readable)
+ if encode and self.debuglevel > 0:
+ print("encoding file using iso-8859-1")
+ while True:
+ datablock = readable.read(blocksize)
+ if not datablock:
+ break
+ if encode:
+ datablock = datablock.encode("iso-8859-1")
+ yield datablock
+
+ def _send_output(self, message_body=None, encode_chunked=False):
"""Send the currently buffered request and clear the buffer.
Appends an extra \\r\\n to the buffer.
@@ -1033,10 +1086,49 @@ class HTTPConnection:
self._buffer.extend((b"", b""))
msg = b"\r\n".join(self._buffer)
del self._buffer[:]
-
self.send(msg)
+
if message_body is not None:
- self.send(message_body)
+
+ # create a consistent interface to message_body
+ if hasattr(message_body, 'read'):
+ # Let file-like take precedence over byte-like. This
+ # is needed to allow the current position of mmap'ed
+ # files to be taken into account.
+ chunks = self._read_readable(message_body)
+ else:
+ try:
+ # this is solely to check to see if message_body
+ # implements the buffer API. it /would/ be easier
+ # to capture if PyObject_CheckBuffer was exposed
+ # to Python.
+ memoryview(message_body)
+ except TypeError:
+ try:
+ chunks = iter(message_body)
+ except TypeError:
+ raise TypeError("message_body should be a bytes-like "
+ "object or an iterable, got %r"
+ % type(message_body))
+ else:
+ # the object implements the buffer interface and
+ # can be passed directly into socket methods
+ chunks = (message_body,)
+
+ for chunk in chunks:
+ if not chunk:
+ if self.debuglevel > 0:
+ print('Zero length chunk ignored')
+ continue
+
+ if encode_chunked and self._http_vsn == 11:
+ # chunked encoding
+ chunk = '{0:X}\r\n'.format(len(chunk)).encode('ascii') + chunk + b'\r\n'
+ self.send(chunk)
+
+ if encode_chunked and self._http_vsn == 11:
+ # end chunked transfer
+ self.send(b'0\r\n\r\n')
def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0):
"""Send a request to the server.
@@ -1189,24 +1281,23 @@ class HTTPConnection:
header = header + b': ' + value
self._output(header)
- def endheaders(self, message_body=None):
+ def endheaders(self, message_body=None, *, encode_chunked=False):
"""Indicate that the last header line has been sent to the server.
This method sends the request to the server. The optional message_body
argument can be used to pass a message body associated with the
- request. The message body will be sent in the same packet as the
- message headers if it is a string, otherwise it is sent as a separate
- packet.
+ request.
"""
if self.__state == _CS_REQ_STARTED:
self.__state = _CS_REQ_SENT
else:
raise CannotSendHeader()
- self._send_output(message_body)
+ self._send_output(message_body, encode_chunked=encode_chunked)
- def request(self, method, url, body=None, headers={}):
+ def request(self, method, url, body=None, headers={}, *,
+ encode_chunked=False):
"""Send a complete request to the server."""
- self._send_request(method, url, body, headers)
+ self._send_request(method, url, body, headers, encode_chunked)
def _set_content_length(self, body, method):
# Set the content-length based on the body. If the body is "empty", we
@@ -1232,9 +1323,9 @@ class HTTPConnection:
if thelen is not None:
self.putheader('Content-Length', thelen)
- def _send_request(self, method, url, body, headers):
+ def _send_request(self, method, url, body, headers, encode_chunked):
# Honor explicitly requested Host: and Accept-Encoding: headers.
- header_names = dict.fromkeys([k.lower() for k in headers])
+ header_names = frozenset(k.lower() for k in headers)
skips = {}
if 'host' in header_names:
skips['skip_host'] = 1
@@ -1243,15 +1334,40 @@ class HTTPConnection:
self.putrequest(method, url, **skips)
+ # chunked encoding will happen if HTTP/1.1 is used and either
+ # the caller passes encode_chunked=True or the following
+ # conditions hold:
+ # 1. content-length has not been explicitly set
+ # 2. the body is a file or iterable, but not a str or bytes-like
+ # 3. Transfer-Encoding has NOT been explicitly set by the caller
+
if 'content-length' not in header_names:
- self._set_content_length(body, method)
+ # only chunk body if not explicitly set for backwards
+ # compatibility, assuming the client code is already handling the
+ # chunking
+ if 'transfer-encoding' not in header_names:
+ # if content-length cannot be automatically determined, fall
+ # back to chunked encoding
+ encode_chunked = False
+ content_length = self._get_content_length(body, method)
+ if content_length is None:
+ if body is not None:
+ if self.debuglevel > 0:
+ print('Unable to determine size of %r' % body)
+ encode_chunked = True
+ self.putheader('Transfer-Encoding', 'chunked')
+ else:
+ self.putheader('Content-Length', str(content_length))
+ else:
+ encode_chunked = False
+
for hdr, value in headers.items():
self.putheader(hdr, value)
if isinstance(body, str):
# RFC 2616 Section 3.7.1 says that text default has a
# default charset of iso-8859-1.
body = _encode(body, 'body')
- self.endheaders(body)
+ self.endheaders(body, encode_chunked=encode_chunked)
def getresponse(self):
"""Get the response from the server.
diff --git a/tests/green_http_test.py b/tests/green_http_test.py
index fc143fe..226f66e 100644
--- a/tests/green_http_test.py
+++ b/tests/green_http_test.py
@@ -1,3 +1,4 @@
+import eventlet
from eventlet.support import six
import tests
@@ -10,3 +11,14 @@ def test_green_http_doesnt_change_original_module():
def test_green_httplib_doesnt_change_original_module():
tests.run_isolated('green_httplib_doesnt_change_original_module.py')
+
+
+def test_http_request_encode_chunked_kwarg():
+ # https://bugs.python.org/issue12319
+ # As of 2017-01 this test only verifies encode_chunked kwarg is properly accepted.
+ # Stdlib http.client code was copied partially, chunked encoding may not work.
+ from eventlet.green.http import client
+ server_sock = eventlet.listen(('127.0.0.1', 0))
+ addr = server_sock.getsockname()
+ h = client.HTTPConnection(host=addr[0], port=addr[1])
+ h.request('GET', '/', encode_chunked=True)