diff options
author | Sergey Shepelev <temotor@gmail.com> | 2017-01-05 07:25:01 +0300 |
---|---|---|
committer | Sergey Shepelev <temotor@gmail.com> | 2017-01-05 09:06:15 +0300 |
commit | 7595a5a4b789ce9492f88c96a412cdf1e47e60cc (patch) | |
tree | 5e2e235f9a871e9bb54792553d4080f623574ecf | |
parent | ed79125c652d7ea02cb79a84c1292fdc19932a94 (diff) | |
download | eventlet-7595a5a4b789ce9492f88c96a412cdf1e47e60cc.tar.gz |
python3.6: http.client.request support chunked_encoding
https://bugs.python.org/issue12319
-rw-r--r-- | eventlet/green/http/client.py | 144 | ||||
-rw-r--r-- | tests/green_http_test.py | 12 |
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) |