Rev 441: include HeadMiddleware so that we can be sure HEAD requests never return BODY content. in http://bazaar.launchpad.net/~loggerhead-team/loggerhead/trunk-rich

John Arbash Meinel john at arbash-meinel.com
Sat Mar 19 08:36:23 UTC 2011


At http://bazaar.launchpad.net/~loggerhead-team/loggerhead/trunk-rich

------------------------------------------------------------
revno: 441 [merge]
revision-id: john at arbash-meinel.com-20110319083557-k8mbbkr3bzisz3ob
parent: john at arbash-meinel.com-20110318093901-56t8eepvwp2avnaz
parent: john at arbash-meinel.com-20110210023315-515pkynlfpfs3cvm
committer: John Arbash Meinel <john at arbash-meinel.com>
branch nick: trunk-rich
timestamp: Sat 2011-03-19 09:35:57 +0100
message:
  include HeadMiddleware so that we can be sure HEAD requests never return BODY content.
added:
  loggerhead/apps/http_head.py   http_head.py-20110210021453-bsk2uowiyifp730j-1
  loggerhead/tests/test_http_head.py test_http_head.py-20110210021453-bsk2uowiyifp730j-2
modified:
  NEWS                           news-20070121024650-6cwmhprgtcegpxvm-1
  __init__.py                    __init__.py-20090123174919-ekabddqbmvwuci2i-1
  loggerhead/tests/__init__.py   __init__.py-20061211064342-102iqirsciyvgtcf-29
  loggerhead/tests/test_simple.py test_simple.py-20070523103003-hrxwg5n1hcebw7l6-1
-------------- next part --------------
=== modified file 'NEWS'
--- a/NEWS	2011-03-16 14:43:36 +0000
+++ b/NEWS	2011-03-19 08:35:57 +0000
@@ -9,6 +9,12 @@
       multiple threads, and issue concurrent requests.
       (John Arbash Meinel)
 
+    - HEAD requests should not return body content. This is done by adding
+      another wsgi middleware that strips the body when the REQUEST_METHOD is
+      HEAD. Note that you have to add the middleware into your pipeline, and
+      it does not decrease the actual work done.
+      (John Arbash Meinel, #716201)
+
     - If we get a HEAD request, there is no reason to expand the template, we
       shouldn't be returning body content anyway.
       (John Arbash Meinel, #716201, #716217)

=== modified file '__init__.py'
--- a/__init__.py	2011-03-16 12:29:36 +0000
+++ b/__init__.py	2011-03-19 08:35:57 +0000
@@ -62,11 +62,16 @@
             sys.path.append(os.path.dirname(__file__))
 
     def serve_http(transport, host=None, port=None, inet=None):
+        # TODO: if we supported inet to pass requests in and respond to them,
+        #       then it would be easier to test the full stack, but it probably
+        #       means routing around paste.httpserver.serve which probably
+        #       isn't testing the full stack
         from paste.httpexceptions import HTTPExceptionHandler
         from paste.httpserver import serve
 
         _ensure_loggerhead_path()
 
+        from loggerhead.apps.http_head import HeadMiddleware
         from loggerhead.apps.transport import BranchesFromTransportRoot
         from loggerhead.config import LoggerheadConfig
         from loggerhead.main import setup_logging
@@ -81,6 +86,7 @@
         config = LoggerheadConfig(argv)
         setup_logging(config, init_logging=False, log_file=sys.stderr)
         app = BranchesFromTransportRoot(transport.base, config)
+        app = HeadMiddleware(app)
         app = HTTPExceptionHandler(app)
         serve(app, host=host, port=port)
 

=== added file 'loggerhead/apps/http_head.py'
--- a/loggerhead/apps/http_head.py	1970-01-01 00:00:00 +0000
+++ b/loggerhead/apps/http_head.py	2011-02-10 02:33:15 +0000
@@ -0,0 +1,62 @@
+# Copyright (C) 2011 Canonical Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+"""WSGI apps tend to return body content as part of a HEAD request.
+
+We should definitely not do that.
+"""
+
+class HeadMiddleware(object):
+    """When we get a HEAD request, we should not return body content.
+
+    WSGI defaults to just generating everything, and not paying attention to
+    whether it is a GET or a HEAD request. It does that because of potential
+    issues getting the Headers correct.
+
+    This middleware works by just omitting the body if the request method is
+    HEAD.
+    """
+
+    def __init__(self, app):
+        self._wrapped_app = app
+        self._real_environ = None
+        self._real_start_response = None
+        self._real_writer = None
+
+    def noop_write(self, chunk):
+        """We intentionally ignore all body content that is returned."""
+        pass
+
+    def start_response(self, status, response_headers, exc_info=None):
+        if exc_info is None:
+            self._real_writer = self._real_start_response(status,
+                response_headers)
+        else:
+            self._real_writer = self._real_start_response(status,
+                response_headers, exc_info)
+        return self.noop_write
+
+    def __call__(self, environ, start_response):
+        self._real_environ = environ
+        self._real_start_response = start_response
+        if environ.get('REQUEST_METHOD', 'GET') == 'HEAD':
+            result = self._wrapped_app(environ, self.start_response)
+            for chunk in result:
+                pass
+        else:
+            result = self._wrapped_app(environ, start_response)
+            for chunk in result:
+                yield chunk

=== modified file 'loggerhead/tests/__init__.py'
--- a/loggerhead/tests/__init__.py	2011-03-16 14:43:36 +0000
+++ b/loggerhead/tests/__init__.py	2011-03-19 08:35:57 +0000
@@ -21,6 +21,7 @@
             'test_controllers',
             'test_corners',
             'test_history',
+            'test_http_head',
             'test_load_test',
             'test_simple',
             'test_revision_ui',

=== added file 'loggerhead/tests/test_http_head.py'
--- a/loggerhead/tests/test_http_head.py	1970-01-01 00:00:00 +0000
+++ b/loggerhead/tests/test_http_head.py	2011-02-10 02:33:15 +0000
@@ -0,0 +1,92 @@
+# Copyright 2011 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+"""Tests for the HeadMiddleware app."""
+
+from cStringIO import StringIO
+
+from bzrlib import tests
+
+from loggerhead.apps import http_head
+
+
+content = ["<html>",
+           "<head><title>Listed</title></head>",
+           "<body>Content</body>",
+           "</html>",
+          ]
+headers = {'X-Ignored-Header': 'Value'}
+
+def yielding_app(environ, start_response):
+    writer = start_response('200 OK', headers)
+    for chunk in content:
+        yield chunk
+
+
+def list_app(environ, start_response):
+    writer = start_response('200 OK', headers)
+    return content
+
+
+def writer_app(environ, start_response):
+    writer = start_response('200 OK', headers)
+    for chunk in content:
+        writer(chunk)
+    return []
+
+
+class TestHeadMiddleware(tests.TestCase):
+
+    def _trap_start_response(self, status, response_headers, exc_info=None):
+        self._write_buffer = StringIO()
+        self._start_response_passed = (status, response_headers, exc_info)
+        return self._write_buffer.write
+
+    def _consume_app(self, app, request_method):
+        environ = {'REQUEST_METHOD': request_method}
+        value = list(app(environ, self._trap_start_response))
+        self._write_buffer.writelines(value)
+
+    def _verify_get_passthrough(self, app):
+        app = http_head.HeadMiddleware(app)
+        self._consume_app(app, 'GET')
+        self.assertEqual(('200 OK', headers, None), self._start_response_passed)
+        self.assertEqualDiff(''.join(content), self._write_buffer.getvalue())
+
+    def _verify_head_no_body(self, app):
+        app = http_head.HeadMiddleware(app)
+        self._consume_app(app, 'HEAD')
+        self.assertEqual(('200 OK', headers, None), self._start_response_passed)
+        self.assertEqualDiff('', self._write_buffer.getvalue())
+
+    def test_get_passthrough_yielding(self):
+        self._verify_get_passthrough(yielding_app)
+
+    def test_head_passthrough_yielding(self):
+        self._verify_head_no_body(yielding_app)
+
+    def test_get_passthrough_list(self):
+        self._verify_get_passthrough(list_app)
+
+    def test_head_passthrough_list(self):
+        self._verify_head_no_body(list_app)
+
+    def test_get_passthrough_writer(self):
+        self._verify_get_passthrough(writer_app)
+
+    def test_head_passthrough_writer(self):
+        self._verify_head_no_body(writer_app)
+

=== modified file 'loggerhead/tests/test_simple.py'
--- a/loggerhead/tests/test_simple.py	2011-03-16 14:43:36 +0000
+++ b/loggerhead/tests/test_simple.py	2011-03-19 08:35:57 +0000
@@ -27,6 +27,7 @@
 from bzrlib import config
 
 from loggerhead.apps.branch import BranchWSGIApp
+from loggerhead.apps.http_head import HeadMiddleware
 from paste.fixture import TestApp
 from paste.httpexceptions import HTTPExceptionHandler, HTTPMovedPermanently
 
@@ -197,6 +198,32 @@
         e = self.assertRaises(HTTPMovedPermanently, app.get, '/files/head:/file')
         self.assertEqual(e.location(), '/view/head:/file')
 
+
+class TestHeadMiddleware(BasicTests):
+
+    def setUp(self):
+        BasicTests.setUp(self)
+        self.createBranch()
+        self.msg = 'trivial commit message'
+        self.revid = self.tree.commit(message=self.msg)
+
+    def setUpLoggerhead(self, **kw):
+        branch_app = BranchWSGIApp(self.tree.branch, '', **kw).app
+        return TestApp(HTTPExceptionHandler(HeadMiddleware(branch_app)))
+
+    def test_get(self):
+        app = self.setUpLoggerhead()
+        res = app.get('/changes')
+        res.mustcontain(self.msg)
+        self.assertEqual('text/html', res.header('Content-Type'))
+
+    def test_head(self):
+        app = self.setUpLoggerhead()
+        res = app.get('/changes', extra_environ={'REQUEST_METHOD': 'HEAD'})
+        self.assertEqual('text/html', res.header('Content-Type'))
+        self.assertEqualDiff('', res.body)
+
+
 #class TestGlobalConfig(BasicTests):
 #    """
 #    Test that global config settings are respected



More information about the bazaar-commits mailing list