From: Ben Darnell Date: Sun, 23 Oct 2011 21:43:20 +0000 (-0700) Subject: Fix hang on HEAD requests and on 204/304 responses with no content-length. X-Git-Tag: v2.2.0~87 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a9e92e216d61d5cc314dc033d53e56f74009b098;p=thirdparty%2Ftornado.git Fix hang on HEAD requests and on 204/304 responses with no content-length. Closes #386. --- diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py index a98eb5448..65a241f5c 100644 --- a/tornado/simple_httpclient.py +++ b/tornado/simple_httpclient.py @@ -306,9 +306,36 @@ class _HTTPConnection(object): assert match self.code = int(match.group(1)) self.headers = HTTPHeaders.parse(header_data) + + if "Content-Length" in self.headers: + if "," in self.headers["Content-Length"]: + # Proxies sometimes cause Content-Length headers to get + # duplicated. If all the values are identical then we can + # use them but if they differ it's an error. + pieces = re.split(r',\s*', self.headers["Content-Length"]) + if any(i != pieces[0] for i in pieces): + raise ValueError("Multiple unequal Content-Lengths: %r" % + self.headers["Content-Length"]) + self.headers["Content-Length"] = pieces[0] + content_length = int(self.headers["Content-Length"]) + else: + content_length = None + if self.request.header_callback is not None: for k, v in self.headers.get_all(): self.request.header_callback("%s: %s\r\n" % (k, v)) + + if self.request.method == "HEAD": + # HEAD requests never have content, even though they may have + # content-length headers + self._on_body(b("")) + if 100 <= self.code < 200 or self.code in (204, 304): + # These response codes never have bodies + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3 + assert "Transfer-Encoding" not in self.headers + assert content_length in (None, 0) + self._on_body(b("")) + if (self.request.use_gzip and self.headers.get("Content-Encoding") == "gzip"): # Magic parameter makes zlib module understand gzip header @@ -317,18 +344,8 @@ class _HTTPConnection(object): if self.headers.get("Transfer-Encoding") == "chunked": self.chunks = [] self.stream.read_until(b("\r\n"), self._on_chunk_length) - elif "Content-Length" in self.headers: - if "," in self.headers["Content-Length"]: - # Proxies sometimes cause Content-Length headers to get - # duplicated. If all the values are identical then we can - # use them but if they differ it's an error. - pieces = re.split(r',\s*', self.headers["Content-Length"]) - if any(i != pieces[0] for i in pieces): - raise ValueError("Multiple unequal Content-Lengths: %r" % - self.headers["Content-Length"]) - self.headers["Content-Length"] = pieces[0] - self.stream.read_bytes(int(self.headers["Content-Length"]), - self._on_body) + elif content_length is not None: + self.stream.read_bytes(content_length, self._on_body) else: self.stream.read_until_close(self._on_body) diff --git a/tornado/test/simple_httpclient_test.py b/tornado/test/simple_httpclient_test.py index cc23d620c..b8c8b3fe1 100644 --- a/tornado/test/simple_httpclient_test.py +++ b/tornado/test/simple_httpclient_test.py @@ -44,6 +44,16 @@ class ContentLengthHandler(RequestHandler): self.set_header("Content-Length", self.get_argument("value")) self.write("ok") +class HeadHandler(RequestHandler): + def head(self): + self.set_header("Content-Length", "7") + +class NoContentHandler(RequestHandler): + def get(self): + if self.get_argument("error", None): + self.set_header("Content-Length", "7") + self.set_status(204) + class SimpleHTTPClientTestCase(AsyncHTTPTestCase, LogTrapTestCase): def get_app(self): # callable objects to finish pending /trigger requests @@ -56,6 +66,8 @@ class SimpleHTTPClientTestCase(AsyncHTTPTestCase, LogTrapTestCase): url("/hang", HangHandler), url("/hello", HelloWorldHandler), url("/content_length", ContentLengthHandler), + url("/head", HeadHandler), + url("/no_content", NoContentHandler), ], gzip=True) def test_singleton(self): @@ -173,3 +185,20 @@ class SimpleHTTPClientTestCase(AsyncHTTPTestCase, LogTrapTestCase): self.assertEqual(response.code, 599) response = self.fetch("/content_length?value=2,%202,3") self.assertEqual(response.code, 599) + + def test_head_request(self): + response = self.fetch("/head", method="HEAD") + self.assertEqual(response.code, 200) + self.assertEqual(response.headers["content-length"], "7") + self.assertFalse(response.body) + + def test_no_content(self): + response = self.fetch("/no_content") + self.assertEqual(response.code, 204) + # 204 status doesn't need a content-length, but tornado will + # add a zero content-length anyway. + self.assertEqual(response.headers["Content-length"], "0") + + # 204 status with non-zero content length is malformed + response = self.fetch("/no_content?error=1") + self.assertEqual(response.code, 599)