From ca7f4d0724fded8317f2e950e1852203e4cb80dd Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Sat, 15 Mar 2014 23:39:59 -0400 Subject: [PATCH] Move chunked-response generation into http1connection. --- tornado/http1connection.py | 34 ++++++++++++++++++++++++++++++++-- tornado/web.py | 34 +--------------------------------- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/tornado/http1connection.py b/tornado/http1connection.py index 2ae402ebc..c2a5eef90 100644 --- a/tornado/http1connection.py +++ b/tornado/http1connection.py @@ -50,6 +50,8 @@ class HTTP1Connection(object): self._clear_request_state() self.stream.set_close_callback(self._on_connection_close) self._finish_future = None + self._version = None + self._chunking = None def start_serving(self, delegate, gzip=False): assert isinstance(delegate, httputil.HTTPServerConnectionDelegate) @@ -88,6 +90,10 @@ class HTTP1Connection(object): start_line = httputil.parse_response_start_line(start_line) else: start_line = httputil.parse_request_start_line(start_line) + # It's kind of ugly to set this here, but we need it in + # write_header() so we know whether we can chunk the response. + self._version = start_line.version + self._disconnect_on_finish = not self._can_keep_alive( start_line, headers) ret = delegate.headers_received(start_line, headers) @@ -159,21 +165,44 @@ class HTTP1Connection(object): self._clear_request_state() def write_headers(self, start_line, headers): + self._chunking = ( + # TODO: should this use self._version or start_line.version? + self._version == 'HTTP/1.1' and + # 304 responses have no body (not even a zero-length body), and so + # should not have either Content-Length or Transfer-Encoding. + # headers. + start_line.code != 304 and + # No need to chunk the output if a Content-Length is specified. + 'Content-Length' not in headers and + # Applications are discouraged from touching Transfer-Encoding, + # but if they do, leave it alone. + 'Transfer-Encoding' not in headers) + if self._chunking: + headers['Transfer-Encoding'] = 'chunked' lines = [utf8("%s %s %s" % start_line)] lines.extend([utf8(n) + b": " + utf8(v) for n, v in headers.get_all()]) for line in lines: if b'\n' in line: raise ValueError('Newline in header: ' + repr(line)) - self.write(b"\r\n".join(lines) + b"\r\n\r\n") + if not self.stream.closed(): + self.stream.write(b"\r\n".join(lines) + b"\r\n\r\n") def write(self, chunk, callback=None): """Writes a chunk of output to the stream.""" + if self._chunking and chunk: + # Don't write out empty chunks because that means END-OF-STREAM + # with chunked encoding + chunk = utf8("%x" % len(chunk)) + b"\r\n" + chunk + b"\r\n" if not self.stream.closed(): self._write_callback = stack_context.wrap(callback) self.stream.write(chunk, self._on_write_complete) def finish(self): """Finishes the request.""" + if self._chunking: + if not self.stream.closed(): + self.stream.write(b"0\r\n\r\n", self._on_write_complete) + self._chunking = False self._request_finished = True # No more data is coming, so instruct TCP to send any remaining # data immediately instead of waiting for a full packet or ack. @@ -227,7 +256,8 @@ class HTTP1Connection(object): headers = httputil.HTTPHeaders.parse(data[eol:]) except ValueError: # probably form split() if there was no ':' in the line - raise httputil.HTTPMessageException("Malformed HTTP headers") + raise httputil.HTTPMessageException("Malformed HTTP headers: %r" % + data[eol:100]) return start_line, headers def _read_body(self, is_client, headers, delegate): diff --git a/tornado/web.py b/tornado/web.py index 31da41bee..424e35449 100644 --- a/tornado/web.py +++ b/tornado/web.py @@ -1480,7 +1480,6 @@ class Application(object): self.transforms = [] if settings.get("gzip"): self.transforms.append(GZipContentEncoding) - self.transforms.append(ChunkedTransferEncoding) else: self.transforms = transforms self.handlers = [] @@ -2262,7 +2261,7 @@ class OutputTransform(object): """A transform modifies the result of an HTTP request (e.g., GZip encoding) A new transform instance is created for every request. See the - ChunkedTransferEncoding example below if you want to implement a + GZipContentEncoding example below if you want to implement a new Transform. """ def __init__(self, request): @@ -2327,37 +2326,6 @@ class GZipContentEncoding(OutputTransform): return chunk -class ChunkedTransferEncoding(OutputTransform): - """Applies the chunked transfer encoding to the response. - - See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1 - """ - def __init__(self, request): - self._chunking = request.supports_http_1_1() - - def transform_first_chunk(self, status_code, headers, chunk, finishing): - # 304 responses have no body (not even a zero-length body), and so - # should not have either Content-Length or Transfer-Encoding headers. - if self._chunking and status_code != 304: - # No need to chunk the output if a Content-Length is specified - if "Content-Length" in headers or "Transfer-Encoding" in headers: - self._chunking = False - else: - headers["Transfer-Encoding"] = "chunked" - chunk = self.transform_chunk(chunk, finishing) - return status_code, headers, chunk - - def transform_chunk(self, block, finishing): - if self._chunking: - # Don't write out empty chunks because that means END-OF-STREAM - # with chunked encoding - if block: - block = utf8("%x" % len(block)) + b"\r\n" + block + b"\r\n" - if finishing: - block += b"0\r\n\r\n" - return block - - def authenticated(method): """Decorate methods with this to require that the user be logged in. -- 2.47.2