feat(Api): Catch and log non-HTTPError exceptions thrown by responders

This commit is contained in:
Kurt Griffiths
2013-01-15 14:57:01 -05:00
parent 329446a993
commit c0da157345
10 changed files with 144 additions and 22 deletions

View File

@@ -64,29 +64,32 @@ class API:
req = Request(env)
resp = Response()
path = req.path
for path_template, method_map in self._routes:
m = path_template.match(path)
if m:
req._params.update(m.groupdict())
try:
responder = method_map[req.method]
except KeyError:
responder = responders.bad_request
break
else:
responder = responders.path_not_found
responder, params = self._get_responder(req.path, req.method)
req._params.update(params)
try:
responder(req, resp)
except HTTPError as ex:
resp.status = ex.status
if req.client_accepts_json():
resp.body = ex.json()
except Exception as ex:
# Reset to a known state and respond with a generic error
req = Request(env)
resp = Response()
message = 'Responder raised ' + ex.__class__.__name__
details = str(ex)
if details:
message = ': '.join([message, details])
req.log_error(message)
responders.server_error(req, resp)
#
# Set status and headers
#
@@ -127,3 +130,33 @@ class API:
method_map = create_http_method_map(resource)
self._routes.append((path_template, method_map))
def _get_responder(self, path, method):
"""Searches routes for a matching responder
Args:
path: URI path to search (without query stirng)
method: HTTP method (uppercase) requested
Returns:
A 2-member tuple, containing a responder callable and a dict
containing parsed path fields, if any were specified in
the matching route's URI template
"""
params = {}
for path_template, method_map in self._routes:
m = path_template.match(path)
if m:
params.update(m.groupdict())
try:
responder = method_map[method]
except KeyError:
responder = responders.bad_request
break
else:
responder = responders.path_not_found
return (responder, params)

View File

@@ -16,6 +16,9 @@ limitations under the License.
"""
import sys
from datetime import datetime
from .request_helpers import *
from .exceptions import *
@@ -31,7 +34,8 @@ class Request:
'_params',
'path',
'protocol',
'query_string'
'query_string',
'_wsgierrors'
)
def __init__(self, env):
@@ -53,6 +57,21 @@ class Request:
self.query_string = query_string = env['QUERY_STRING']
self._params = parse_query_string(query_string)
self._headers = parse_headers(env)
self._wsgierrors = env['wsgi.errors']
def log_error(self, message):
if sys.version_info[0] == 2 and isinstance(message, str):
unicode_message = message.decode('utf-8')
else:
unicode_message = message
log_line = (
u'{0:%Y:%m:%d %H:%M:%S} [FALCON] [ERROR] {1} {2}?{3} => {4}'.
format(datetime.now(), self.method, self.path, self.query_string,
unicode_message)
)
self._wsgierrors.write(log_line)
def client_accepts_json(self):
"""Return True if the Accept header indicates JSON support"""

View File

@@ -29,6 +29,11 @@ def bad_request(req, resp):
resp.status = HTTP_400
def server_error(req, resp):
"""Sets response to "500 Internal Server Error", no body."""
resp.status = HTTP_500
def create_method_not_allowed(allowed_methods):
"""Creates a responder for "405 Method Not Allowed".ipyth

View File

@@ -16,6 +16,8 @@ limitations under the License.
"""
import sys
class Response:
"""Represents an HTTP response to a client request"""
@@ -23,10 +25,14 @@ class Response:
__slots__ = ('status', '_headers', 'body', 'stream', 'stream_len')
def __init__(self):
"""Initialize response attributes to default values"""
"""Initialize response attributes to default values
Args:
wsgierrors: File-like stream for logging errors
"""
self.status = None
self._headers = {}
self.body = None

21
foo.py Normal file
View File

@@ -0,0 +1,21 @@
import json
import logging
import falcon
class StorageEngine:
pass
class ThingsResource:
def __init__(self):
pass
def on_get(self, req, resp):
raise IOError()
app = api = falcon.API()
things = ThingsResource()
api.add_route('/{user_id}/things', things)

1
spam Normal file
View File

@@ -0,0 +1 @@
this works

1
spam.txt Normal file
View File

@@ -0,0 +1 @@
cat

View File

@@ -4,8 +4,8 @@ def application(environ, start_response):
body = '\n{\n'
for key, value in environ.items():
if isinstance(value, str):
body += ' "{0}": "{1}",\n'.format(key, value)
#if isinstance(value, str):
body += ' "{0}": "{1}",\n'.format(key, value)
body += '}\n\n'

View File

@@ -1,5 +1,5 @@
import random
from io import BytesIO
import io
import testtools
@@ -73,7 +73,8 @@ class TestSuite(testtools.TestCase):
def create_environ(path='/', query_string='', protocol='HTTP/1.1', port='80',
headers=None, script='', body='', method='GET'):
headers=None, script='', body='', method='GET',
wsgierrors=None):
env = {
'SERVER_PROTOCOL': protocol,
@@ -92,7 +93,8 @@ def create_environ(path='/', query_string='', protocol='HTTP/1.1', port='80',
'SERVER_PORT': port,
'wsgi.url_scheme': 'http',
'wsgi.input': BytesIO(body.encode('utf-8'))
'wsgi.input': io.BytesIO(body.encode('utf-8')),
'wsgi.errors': wsgierrors or io.StringIO()
}
if protocol != 'HTTP/1.0':

34
test/test_wsgi_errors.py Normal file
View File

@@ -0,0 +1,34 @@
import io
from . import helpers
class BombResource:
def on_get(self, req, resp):
raise IOError()
def on_head(self, req, resp):
raise MemoryError()
class LoggerResource:
def on_get(self, req, resp):
pass
class TestWSGIError(helpers.TestSuite):
def prepare(self):
self.tehbomb = BombResource()
self.tehlogger = LoggerResource()
self.api.add_route('/bomb', self.tehbomb)
self.api.add_route('/logger', self.tehlogger)
self.wsgierrors = io.StringIO()
def test_exception_logged(self):
self._simulate_request('/bomb', wsgierrors=self.wsgierrors)
log = self.wsgierrors.getvalue()
self.assertIn(u'Responder raised IOError', log)