feat: Add Content-Length and Connection headers to the response (automatically)

This commit is contained in:
kurt-griffiths
2013-01-02 18:06:37 -05:00
parent 9b9eae7069
commit 21ccd5baa2
7 changed files with 140 additions and 54 deletions

View File

@@ -13,7 +13,7 @@ class Api:
def __init__(self):
self.routes = {}
def __call__(self, environ, start_response):
def __call__(self, env, start_response):
"""WSGI protocol handler"""
# PERF: Use literal constructor for dicts
@@ -22,51 +22,27 @@ class Api:
# TODO
# ctx.update(global_ctx_for_route)
path = environ['PATH_INFO']
req = Request(path)
req = Request(env)
resp = Response()
# PERF: Use try...except blocks when the key usually exists
try:
# TODO: Figure out a way to use codegen to make a state machine,
# may have to in order to support URI templates.
handler = self.routes[path]
handler = self.routes[req.path]
except KeyError:
handler = path_not_found_handler
try:
handler(ctx, req, resp)
except Exception as ex:
# TODO
pass
handler(ctx, req, resp)
resp.set_header('Content-Type', 'text/plain')
#
# Set status and headers
#
self._set_auto_resp_headers(env, req, resp)
start_response(resp.status, resp._wsgi_headers())
# Consider refactoring into functions, but be careful since that can
# affect performance...
body = resp.body
content_length = 0
try:
if body is not None:
content_length = len(body)
except Exception as ex:
#TODO
pass
resp.set_header('Content-Length', content_length)
headers = resp._wsgi_headers()
try:
start_response(resp.status, headers)
except Exception as ex:
# TODO
pass
# PERF: Can't predict ratio of empty body to nonempty, so use
# "in" which is a good all-around performer.
return [body] if body is not None else []
# Return an iterable for the body, per the WSGI spec
return [resp.body] if resp.body is not None else []
def add_route(self, uri_template, handler):
self.routes[uri_template] = handler
@@ -76,4 +52,27 @@ class Api:
# Helpers
# -------------------------------------------------------------------------
def _set_auto_resp_headers(self, env, req, resp):
resp.set_header('Content-Type', 'text/plain')
# Set Content-Length when given a fully-buffered body
if resp.body is not None:
resp.set_header('Content-Length', 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

@@ -1,5 +1,28 @@
class Request:
__slots__ = ('path')
__slots__ = ('path', 'headers')
def __init__(self, env):
self.path = env['PATH_INFO']
self.headers = headers = {}
# Extract HTTP headers
for key in env:
if key.startswith('HTTP_'):
headers[key[5:]] = env[key]
def get_header(self, name, default=None):
"""Return a header value as a string
name -- Header name, case-insensitive
default -- Value to return in case the header is not found
"""
headers = self.headers
# Optimize for the header existing in most cases
try:
return headers[name.upper()]
except KeyError as e:
return default
def __init__(self, path):
self.path = path

View File

@@ -1,11 +1,14 @@
class Response:
__slots__ = ('status', '_headers', 'body', 'stream')
__slots__ = ('status', '_headers', 'body', 'stream', 'stream_len')
def __init__(self):
self.status = None
self._headers = {}
self.body = None
self.stream = None
self.stream_len = None
def set_header(self, name, value):
self._headers[name] = str(value)
@@ -13,4 +16,4 @@ class Response:
#TODO: Add some helper functions and test them
def _wsgi_headers(self):
return [t for t in self._headers.items()]
return [t for t in self._headers.items()]

View File

@@ -1,3 +1,4 @@
def application(environ, start_response):
start_response("200 OK", [('Content-Type', 'text/plain')])
@@ -10,7 +11,9 @@ def application(environ, start_response):
return body
import gevent, gevent.wsgi
server = gevent.wsgi.WSGIServer(('localhost', 8000), application)
server.serve_forever()
if __name__ == '__main__':
from wsgiref.simple_server import make_server
server = make_server('localhost', 8000, application)
server.serve_forever()

View File

@@ -31,8 +31,9 @@ class TestSuite(testtools.TestCase):
if hasattr(prepare, '__call__'):
prepare()
def _simulate_request(self, path):
self.api(create_environ(path), self.srmock)
def _simulate_request(self, path, protocol='HTTP/1.1', headers=None):
self.api(create_environ(path=path, protocol=protocol, headers=headers),
self.srmock)
class RandChars:
_chars = 'abcdefghijklqmnopqrstuvwxyz0123456789 \n\t!@#$%^&*()-_=+`~<>,.?/'
@@ -54,12 +55,12 @@ class RandChars:
def rand_string(min, max):
return ''.join([c for c in RandChars(min, max)])
def create_environ(path='/', query_string=''):
return {
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': 'HTTP/1.1',
'SERVER_PROTOCOL': protocol,
'HOME': '/Users/kurt',
'DISPLAY': '/tmp/launch-j5GrQm/org.macosforge.xquartz:0',
'TERM_PROGRAM': 'Apple_Terminal',
@@ -100,3 +101,9 @@ def create_environ(path='/', query_string=''):
'REMOTE_HOST': '1.0.0.127.in-addr.arpa',
'COMMAND_MODE': 'unix2003'
}
if headers is not None:
for name, value in headers.iteritems():
env['HTTP_' + name.upper()] = value.strip()
return env

View File

@@ -1,12 +1,14 @@
import testtools
import random
from testtools.matchers import Equals, MatchesRegex, Contains
from testtools.matchers import Equals, MatchesRegex, Contains, Not
import falcon
import test.helpers as helpers
# TODO: Framework adds keep-alive and either chunked or content-length
# TODO: Automatically set text encoding to UTF-8 for plaintext (?)
# TODO: Chunked when streams are set
# TODO: Test setting various headers, and seeing that Falcon doesn't override custom ones, but will set them if not present (or not?)
# TODO: Test correct content length is set
# TODO: Test calling set_header with bogus arguments
@@ -14,6 +16,9 @@ import test.helpers as helpers
# TODO: Helper functions for getting and setting common headers
# TODO: Any default headers, such as content-type?
# TODO: if status is 1xx, 204, or 404 ignore body, don't set content-length
# TODO: Test passing through all headers in req object (HTTP_* in WSGI env) - better to do lazy eval
# TODO: Header names must be lower-case on lookup - test bogus, defaults
# TODO: HTTP_HOST, if present, should be used in preference to SERVER_NAME
class RequestHandler:
sample_status = "200 OK"
@@ -36,12 +41,48 @@ class TestHeaders(helpers.TestSuite):
self.on_hello = RequestHandler()
self.api.add_route(self.test_route, self.on_hello)
def test_auto_headers(self):
def test_content_length(self):
self._simulate_request(self.test_route)
headers = self.srmock.headers
content_length = ('Content-Length', str(len(self.on_hello.sample_body)))
self.assertThat(headers, Contains(content_length))
# Test Content-Length header set
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

@@ -1,9 +1,14 @@
Functionality
* Test sending/receiving various status codes
* Chunked transfer encoding, streaming 0, some, and lots of bytes
* Add tips to NOTES.md
* Framework should automatically set content-length for smaller streams, and only enable chunked encoding for really big ones AND when *not* using HTTP/1.0.
* 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)
* Test setting the body to a stream, rather than a string (and content-length set to chunked?)
* Figure out encoding of body and stream - test that the framework requires byte strings or a byte string generator (for body and stream attributes of resp, respectively)
* Test custom error handlers - customizing error document at least
* Test async middleware ala rproxy
* Test setting different routes for different verbs
@@ -17,4 +22,9 @@
* Test pre/post filters
* Test error handling with standard response (for all error classes?)
* Test passing bogus data to create_api and/or add_route
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)