Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

304 responses no longer include entity headers like Content-Length

This is required by the RFC as it may confuse caches.
  • Loading branch information...
commit b1df6635876022252d73fc1c70a0e8682b4bedf9 1 parent dd76559
@bdarnell bdarnell authored
Showing with 78 additions and 2 deletions.
  1. +51 −0 tornado/test/web_test.py
  2. +27 −2 tornado/web.py
View
51 tornado/test/web_test.py
@@ -15,6 +15,16 @@
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 @@ def _trigger_include_host_check(self, include_host):
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 @@ def test_named_urlspec_groups(self):
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)
View
29 tornado/web.py
@@ -264,6 +264,15 @@ def add_header(self, name, value):
"""
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 @@ def finish(self, chunk=None):
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 @@ def render(*args, **kwargs):
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 @@ def __init__(self, request):
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
Please sign in to comment.
Something went wrong with that request. Please try again.