feat: Add Content-Length and Connection headers to the response (automatically)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
|
||||
12
test/todo.md
12
test/todo.md
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user