Rev 424: Fix bug #716201 by suppressing body content when getting a HEAD request. in http://bazaar.launchpad.net/~jameinel/loggerhead/head_middleware
John Arbash Meinel
john at arbash-meinel.com
Thu Feb 10 02:33:16 UTC 2011
At http://bazaar.launchpad.net/~jameinel/loggerhead/head_middleware
------------------------------------------------------------
revno: 424
revision-id: john at arbash-meinel.com-20110210023315-515pkynlfpfs3cvm
parent: john at arbash-meinel.com-20110210015817-v1ojc0vsvxjq20u2
fixes bug(s): https://launchpad.net/bugs/716201
committer: John Arbash Meinel <john at arbash-meinel.com>
branch nick: head_middleware
timestamp: Wed 2011-02-09 20:33:15 -0600
message:
Fix bug #716201 by suppressing body content when getting a HEAD request.
This adds some WSGI middleware that suppresses returning body content if a HEAD request
is received.
Note that we don't yet pass GET down to the lower levels, so they could still
decide whether they can do less work or not. We may want to make the standard BranchWSGIApp
do less work under those circumstances.
-------------- next part --------------
=== modified file 'NEWS'
--- a/NEWS 2010-11-11 03:30:43 +0000
+++ b/NEWS 2011-02-10 02:33:15 +0000
@@ -1,6 +1,15 @@
What's changed in loggerhead?
=============================
+1.19 [????]
+-----------
+
+ - 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)
+
1.18 [10Nov2010]
----------------
=== modified file '__init__.py'
--- a/__init__.py 2010-11-11 02:48:30 +0000
+++ b/__init__.py 2011-02-10 02:33:15 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009, 2010 Canonical Ltd
+# Copyright 2009, 2010, 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
@@ -82,11 +82,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
@@ -100,6 +105,7 @@
config = LoggerheadConfig(argv)
setup_logging(config)
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 2010-05-10 19:36:37 +0000
+++ b/loggerhead/tests/__init__.py 2011-02-10 02:33:15 +0000
@@ -1,4 +1,4 @@
-# Copyright 2006, 2010 Canonical Ltd
+# Copyright 2006, 2010, 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
@@ -20,6 +20,7 @@
(__name__ + '.' + x) for x in [
'test_controllers',
'test_corners',
+ 'test_http_head',
'test_simple',
'test_templating',
]]))
=== 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 2009-06-08 23:02:49 +0000
+++ b/loggerhead/tests/test_simple.py 2011-02-10 02:33:15 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2008, 2009 Canonical Ltd.
+# Copyright (C) 2007, 2008, 2009, 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
@@ -23,6 +23,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
@@ -157,6 +158,31 @@
res = app.get('/changes', status=404)
+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