]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
304 responses no longer include entity headers like Content-Length
authorBen Darnell <ben@bendarnell.com>
Sun, 20 May 2012 23:51:02 +0000 (16:51 -0700)
committerBen Darnell <ben@bendarnell.com>
Sun, 20 May 2012 23:51:02 +0000 (16:51 -0700)
This is required by the RFC as it may confuse caches.

tornado/test/web_test.py
tornado/web.py

index ff2f9f80b56b6bd099ba80949fdc3d93863b781e..8d3675b978aeb83b92147b3a8df7db773e98bbe5 100644 (file)
@@ -15,6 +15,16 @@ import socket
 import sys
 
 
+class SimpleHandlerTestCase(AsyncHTTPTestCase):
+    """Simplified base class for tests that work with a single handler class.
+
+    To use, define a nested class named ``Handler``.
+    """
+    def get_app(self):
+        return Application([('/', self.Handler)],
+                           log_function=lambda x: None)
+
+
 class CookieTestRequestHandler(RequestHandler):
     # stub out enough methods to make the secure_cookie functions work
     def __init__(self):
@@ -714,6 +724,14 @@ class StaticFileTest(AsyncHTTPTestCase, LogTrapTestCase):
         response = self.fetch(path % int(include_host))
         self.assertEqual(response.body, utf8(str(True)))
 
+    def test_static_304(self):
+        response1 = self.fetch("/static/robots.txt")
+        response2 = self.fetch("/static/robots.txt", headers={
+                'If-Modified-Since': response1.headers['Last-Modified']})
+        self.assertEqual(response2.code, 304)
+        self.assertTrue('Content-Length' not in response2.headers)
+        self.assertTrue('Last-Modified' not in response2.headers)
+
 
 class CustomStaticFileTest(AsyncHTTPTestCase, LogTrapTestCase):
     def get_app(self):
@@ -769,3 +787,36 @@ class NamedURLSpecGroupsTest(AsyncHTTPTestCase, LogTrapTestCase):
 
         response = self.fetch("/unicode/bar")
         self.assertEqual(response.body, b("bar"))
+
+class ClearHeaderTest(SimpleHandlerTestCase):
+    class Handler(RequestHandler):
+        def get(self):
+            self.set_header("h1", "foo")
+            self.set_header("h2", "bar")
+            self.clear_header("h1")
+            self.clear_header("nonexistent")
+
+    def test_clear_header(self):
+        response = self.fetch("/")
+        self.assertTrue("h1" not in response.headers)
+        self.assertEqual(response.headers["h2"], "bar")
+        
+
+class Header304Test(SimpleHandlerTestCase):
+    class Handler(RequestHandler):
+        def get(self):
+            self.set_header("Content-Language", "en_US")
+            self.write("hello")
+            
+    def test_304_headers(self):
+        response1 = self.fetch('/')
+        self.assertEqual(response1.headers["Content-Length"], "5")
+        self.assertEqual(response1.headers["Content-Language"], "en_US")
+        
+        response2 = self.fetch('/', headers={
+                'If-None-Match': response1.headers["Etag"]})
+        self.assertEqual(response2.code, 304)
+        self.assertTrue("Content-Length" not in response2.headers)
+        self.assertTrue("Content-Language" not in response2.headers)
+        # Not an entity header, but should not be added to 304s by chunking
+        self.assertTrue("Transfer-Encoding" not in response2.headers)
index b5fe74d4ef1707bf1028942780467ec9b2798121..b90324f98117d2fc824f085bf1b265acd60f7f84 100644 (file)
@@ -264,6 +264,15 @@ class RequestHandler(object):
         """
         self._list_headers.append((name, self._convert_header_value(value)))
 
+    def clear_header(self, name):
+        """Clears an outgoing header, undoing a previous `set_header` call.
+
+        Note that this method does not apply to multi-valued headers
+        set by `add_header`.
+        """
+        if name in self._headers:
+            del self._headers[name]
+
     def _convert_header_value(self, value):
         if isinstance(value, bytes_type):
             pass
@@ -673,7 +682,10 @@ class RequestHandler(object):
                     if inm and inm.find(etag) != -1:
                         self._write_buffer = []
                         self.set_status(304)
-            if "Content-Length" not in self._headers:
+            if self._status_code == 304:
+                assert not self._write_buffer, "Cannot send body with 304"
+                self._clear_headers_for_304()
+            elif "Content-Length" not in self._headers:
                 content_length = sum(len(part) for part in self._write_buffer)
                 self.set_header("Content-Length", content_length)
 
@@ -1065,6 +1077,17 @@ class RequestHandler(object):
     def _ui_method(self, method):
         return lambda *args, **kwargs: method(self, *args, **kwargs)
 
+    def _clear_headers_for_304(self):
+        # 304 responses should not contain entity headers (defined in
+        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.1)
+        # not explicitly allowed by
+        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+        headers = ["Allow", "Content-Encoding", "Content-Language",
+                   "Content-Length", "Content-MD5", "Content-Range",
+                   "Content-Type", "Last-Modified"]
+        for h in headers:
+            self.clear_header(h)
+
 
 def asynchronous(method):
     """Wrap request handler methods with this if they are asynchronous.
@@ -1729,7 +1752,9 @@ class ChunkedTransferEncoding(OutputTransform):
         self._chunking = request.supports_http_1_1()
 
     def transform_first_chunk(self, status_code, headers, chunk, finishing):
-        if self._chunking:
+        # 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