fix: WSGI apps must not set hop-by-hop headers per PEP 333

This commit is contained in:
kurt-griffiths
2013-01-03 16:22:18 -05:00
parent 41228e897d
commit a38a10531d
7 changed files with 28 additions and 101 deletions

View File

@@ -1,7 +1,10 @@
* Keep-Alive is intentially disabled for HTTP/1.0 clients to mitigate problems with proxies. See also http://tools.ietf.org/html/rfc2616#section-19.6.2
* resp.set_header assumes second param is a string. App may crash otherwise.
* Don't set content-length. It will only be overridden.
* Header names are case-insensitive in req.get_header
* Set body to a byte string, as per PEP 333 - http://www.python.org/dev/peps/pep-0333/#unicode-issues - if it is textual, it's up to the app to set the proper media type
* If both body and stream are set, body will be used
* For streaming large items, assign a generator to resp.stream that yields data in reasonable chunk sizes (what's a reasonable size?). If stream is set, Falcon will assume Transfer-Encoding: chunked unless you specify stream\_len, in which case Falcon will set Content-Length to stream\_len. Be sure stream_len is accurate; if you lie about it, then clients and possibly your WSGI server) will hang until the connection times out, waiting for stream to generate enough data.
* For streaming large items, assign a generator to resp.stream that yields data in reasonable chunk sizes (what's a reasonable size?). Falcon will then leave off the Content-Length header, and hopefully your WSGI server will do the right thing, assuming you've told it to enable keep-alive (PEP-333 prohibits apps from setting hop-by-hop headers itself, such as Transfer-Encoding).
* If you know the size of the stream in advance, set stream\_len and Falcon will use it to set Content-Length, avoiding the whole chunked encoding issue altogether.

View File

@@ -57,22 +57,10 @@ class Api:
# Set Content-Length when given a fully-buffered body
if resp.body is not None:
resp.set_header('Content-Length', len(resp.body))
resp.set_header('Content-Length', str(len(resp.body)))
elif resp.stream is not None:
# TODO: Transfer-Encoding: chunked
# TODO: if resp.stream_len is not None, don't use chunked
pass
else:
resp.set_header('Content-Length', 0)
# Enable Keep-Alive when appropriate
if env['SERVER_PROTOCOL'] != 'HTTP/1.0':
if req.get_header('Connection') == 'close':
connection = 'close'
else:
connection = 'Keep-Alive'
resp.set_header('Connection', connection)

View File

@@ -13,7 +13,5 @@ class Response:
def set_header(self, name, value):
self._headers[name] = str(value)
#TODO: Add some helper functions and test them
def _wsgi_headers(self):
return [t for t in self._headers.items()]
return self._headers.items()

View File

@@ -1,6 +1,7 @@
def application(environ, start_response):
start_response("200 OK", [('Content-Type', 'text/plain')])
start_response("200 OK", [
('Content-Type', 'text/plain')])
body = '\n{\n'
for key, value in environ.items():
@@ -9,8 +10,9 @@ def application(environ, start_response):
body += '}\n\n'
return body
return [body]
app = application
if __name__ == '__main__':
from wsgiref.simple_server import make_server

View File

@@ -56,50 +56,23 @@ def rand_string(min, max):
return ''.join([c for c in RandChars(min, max)])
def create_environ(path='/', query_string='', protocol='HTTP/1.1', headers=None):
env = {
'SERVER_SOFTWARE': 'WSGIServer/0.1 Python/2.7.3',
'TERM_PROGRAM_VERSION': '309',
'REQUEST_METHOD': 'GET',
'SERVER_PROTOCOL': protocol,
'HOME': '/Users/kurt',
'DISPLAY': '/tmp/launch-j5GrQm/org.macosforge.xquartz:0',
'TERM_PROGRAM': 'Apple_Terminal',
'LANG': 'en_US.UTF-8',
'SHELL': '/bin/bash',
'_': '/Library/Frameworks/Python.framework/Versions/2.7/bin/python',
'SERVER_PORT': '8003',
'HTTP_HOST': 'localhost:8003',
'SCRIPT_NAME': '',
'HTTP_ACCEPT': '*/*',
'wsgi.version': '(1, 0)',
'wsgi.run_once': 'False',
'wsgi.multiprocess': 'False',
'__CF_USER_TEXT_ENCODING': '0x1F5:0:0',
'USER': 'kurt',
'LOGNAME': 'kurt',
'PATH_INFO': path,
'QUERY_STRING': query_string,
'HTTP_USER_AGENT': 'curl/7.24.0 (x86_64-apple-darwin12.0) '
'libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5',
'SERVER_NAME': 'WSGIRef',
'REMOTE_ADDR': '127.0.0.1',
'SHLVL': '1',
'wsgi.url_scheme': 'http',
'CONTENT_LENGTH': '',
'TERM_SESSION_ID': '51EE7744-E45F-455C-AC2E-E232A521094D',
'SSH_AUTH_SOCK': '/tmp/launch-0N2o9o/Listeners',
'Apple_PubSub_Socket_Render': '/tmp/launch-2ZwqSM/Render',
'wsgi.multithread': 'True',
'TMPDIR': '/var/folders/4g/mzp3qmyn33v_xjn1h69y1d3h0000gn/T/',
'LSCOLORS': 'ExCxFxDxBxegedabagaced',
'GATEWAY_INTERFACE': 'CGI/1.1',
'CLICOLOR': '1',
'Apple_Ubiquity_Message': '/tmp/launch-NTB6Mp/Apple_Ubiquity_Message',
'PWD': '/Users/kurt/Projects/rax/falcon',
'CONTENT_TYPE': 'text/plain',
'wsgi.file_wrapper': 'wsgiref.util.FileWrapper',
'REMOTE_HOST': '1.0.0.127.in-addr.arpa',
'COMMAND_MODE': 'unix2003'
"SERVER_PROTOCOL": protocol,
"SERVER_SOFTWARE": "gunicorn/0.17.0",
"SCRIPT_NAME": "",
"REQUEST_METHOD": "GET",
"HTTP_HOST": "localhost:8000",
"PATH_INFO": path,
"QUERY_STRING": query_string,
"HTTP_ACCEPT": "*/*",
"HTTP_USER_AGENT": "curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5",
"REMOTE_PORT": "65133",
"RAW_URI": "/",
"REMOTE_ADDR": "127.0.0.1",
"wsgi.url_scheme": "http",
"SERVER_NAME": "localhost",
"SERVER_PORT": "8000",
}
if headers is not None:

View File

@@ -49,39 +49,3 @@ class TestHeaders(helpers.TestSuite):
content_length = str(len(self.on_hello.sample_body))
content_length_header = ('Content-Length', content_length)
self.assertThat(headers, Contains(content_length_header))
def test_keep_alive_http_1_1(self):
self._simulate_request(self.test_route, protocol='HTTP/1.1')
headers = self.srmock.headers
# Test Keep-Alive assumed on by default (HTTP/1.1)
connection_header = ('Connection', 'Keep-Alive')
self.assertThat(headers, Contains(connection_header))
def test_no_keep_alive_http_1_1(self):
req_headers = {'Connection': 'close'}
self._simulate_request(self.test_route, protocol='HTTP/1.1',
headers=req_headers)
headers = self.srmock.headers
# Test Keep-Alive assumed on by default (HTTP/1.1)
connection_header = ('Connection', 'Keep-Alive')
self.assertThat(headers, Not(Contains(connection_header)))
def test_no_implicit_keep_alive_http_1_0(self):
self._simulate_request(self.test_route, protocol='HTTP/1.0')
headers = self.srmock.headers
# Test Keep-Alive assumed on by default (HTTP/1.1)
connection_header = ('Connection', 'Keep-Alive')
self.assertThat(headers, Not(Contains(connection_header)))
def test_no_explicit_keep_alive_http_1_0(self):
req_headers = {'Connection': 'Keep-Alive'}
self._simulate_request(self.test_route, protocol='HTTP/1.0',
headers=req_headers)
headers = self.srmock.headers
# Test Keep-Alive assumed on by default (HTTP/1.1)
connection_header = ('Connection', 'Keep-Alive')
self.assertThat(headers, Not(Contains(connection_header)))

View File

@@ -2,9 +2,7 @@
Functionality
* Test sending/receiving various status codes
* Chunked transfer encoding, streaming 0, some, and lots of bytes
* Add tips to NOTES.md
* Check that Transfer-Encoding is set unless stream\_len is specified. In the latter case, Content-Length must be tested and found to be equal to stream\_len
transfer encoding
* Ensure req dict has things the app needs, including path, user agent, various headers.
* Test default http status code (200 OK?)
* Test compiling routes, throwing on invalid routes (such as missing initial forward slash or non-ascii)
@@ -30,3 +28,4 @@ Performance
* Test inlining functions, maybe make a tool that does this automatically
* Try using a closure to generate the WSGI request handler (vs. a class)