diff --git a/README.md b/README.md index 0b498bb..cbe5d57 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,14 @@ class StorageEngine: class StorageError(Exception): - pass + @staticmethod + def handle(ex, req, resp, params): + description = ('Sorry, couldn\'t write your thing to the ' + 'database. It worked on my box.') + + raise falcon.HTTPError(falcon.HTTP_725, + 'Database Error', + description) def token_is_valid(token, user_id): @@ -136,8 +143,8 @@ def auth(req, resp, params): token = req.get_header('X-Auth-Token') if token is None: - description = 'Please provide an auth token ' - 'as part of the request.' + description = ('Please provide an auth token ' + 'as part of the request.') raise falcon.HTTPUnauthorized('Auth token required', description, @@ -145,20 +152,20 @@ def auth(req, resp, params): href='http://docs.example.com/auth') if not token_is_valid(token, params['user_id']): - description = 'The provided auth token is not valid. ' - 'Please request a new token and try again.' + description = ('The provided auth token is not valid. ' + 'Please request a new token and try again.') raise falcon.HTTPUnauthorized('Authentication required', description, - scheme=None + scheme=None, href='http://docs.example.com/auth') def check_media_type(req, resp, params): if not req.client_accepts_json: raise falcon.HTTPUnsupportedMediaType( - 'This API only supports the JSON media type.', - href='http://docs.examples.com/api/json') + 'This API only supports the JSON media type.', + href='http://docs.examples.com/api/json') class ThingsResource: @@ -181,9 +188,9 @@ class ThingsResource: 'We appreciate your patience.') raise falcon.HTTPServiceUnavailable( - 'Service Outage', - description, - 30) + 'Service Outage', + description, + 30) resp.set_header('X-Powered-By', 'Donuts') resp.status = falcon.HTTP_200 @@ -206,14 +213,7 @@ class ThingsResource: 'Could not decode the request body. The ' 'JSON was incorrect.') - try: - proper_thing = self.db.add_thing(thing) - - except StorageError: - raise falcon.HTTPError(falcon.HTTP_725, - 'Database Error', - "Sorry, couldn't write your thing to the " - 'database. It worked on my machine.') + proper_thing = self.db.add_thing(thing) resp.status = falcon.HTTP_201 resp.location = '/%s/things/%s' % (user_id, proper_thing.id) @@ -225,10 +225,15 @@ db = StorageEngine() things = ThingsResource(db) app.add_route('/{user_id}/things', things) +# If a responder ever raised an instance of StorageError, pass control to +# the given handler. +app.add_error_handler(StorageError, StorageError.handle) + # Useful for debugging problems in your API; works with pdb.set_trace() if __name__ == '__main__': - httpd = simple_server.make_server('127.0.0.1', 8000, app) - httpd.serve_forever() + httpd = simple_server.make_server('127.0.0.1', 8000, app) + httpd.serve_forever() + ``` diff --git a/README.rst b/README.rst index 843074a..58855ed 100644 --- a/README.rst +++ b/README.rst @@ -145,7 +145,14 @@ Here is a more involved example that demonstrates reading headers and query para class StorageError(Exception): - pass + @staticmethod + def handle(ex, req, resp, params): + description = ('Sorry, couldn\'t write your thing to the ' + 'database. It worked on my box.') + + raise falcon.HTTPError(falcon.HTTP_725, + 'Database Error', + description) def token_is_valid(token, user_id): @@ -157,8 +164,8 @@ Here is a more involved example that demonstrates reading headers and query para token = req.get_header('X-Auth-Token') if token is None: - description = 'Please provide an auth token ' - 'as part of the request.' + description = ('Please provide an auth token ' + 'as part of the request.') raise falcon.HTTPUnauthorized('Auth token required', description, @@ -166,20 +173,20 @@ Here is a more involved example that demonstrates reading headers and query para href='http://docs.example.com/auth') if not token_is_valid(token, params['user_id']): - description = 'The provided auth token is not valid. ' - 'Please request a new token and try again.' + description = ('The provided auth token is not valid. ' + 'Please request a new token and try again.') raise falcon.HTTPUnauthorized('Authentication required', description, - scheme=None + scheme=None, href='http://docs.example.com/auth') def check_media_type(req, resp, params): if not req.client_accepts_json: raise falcon.HTTPUnsupportedMediaType( - 'This API only supports the JSON media type.', - href='http://docs.examples.com/api/json') + 'This API only supports the JSON media type.', + href='http://docs.examples.com/api/json') class ThingsResource: @@ -202,9 +209,9 @@ Here is a more involved example that demonstrates reading headers and query para 'We appreciate your patience.') raise falcon.HTTPServiceUnavailable( - 'Service Outage', - description, - 30) + 'Service Outage', + description, + 30) resp.set_header('X-Powered-By', 'Donuts') resp.status = falcon.HTTP_200 @@ -227,14 +234,7 @@ Here is a more involved example that demonstrates reading headers and query para 'Could not decode the request body. The ' 'JSON was incorrect.') - try: - proper_thing = self.db.add_thing(thing) - - except StorageError: - raise falcon.HTTPError(falcon.HTTP_725, - 'Database Error', - "Sorry, couldn't write your thing to the " - 'database. It worked on my machine.') + proper_thing = self.db.add_thing(thing) resp.status = falcon.HTTP_201 resp.location = '/%s/things/%s' % (user_id, proper_thing.id) @@ -246,10 +246,15 @@ Here is a more involved example that demonstrates reading headers and query para things = ThingsResource(db) app.add_route('/{user_id}/things', things) + # If a responder ever raised an instance of StorageError, pass control to + # the given handler. + app.add_error_handler(StorageError, StorageError.handle) + # Useful for debugging problems in your API; works with pdb.set_trace() if __name__ == '__main__': - httpd = simple_server.make_server('127.0.0.1', 8000, app) - httpd.serve_forever() + httpd = simple_server.make_server('127.0.0.1', 8000, app) + httpd.serve_forever() + Contributing diff --git a/falcon/api.py b/falcon/api.py index 5ab2e0f..526233b 100644 --- a/falcon/api.py +++ b/falcon/api.py @@ -82,7 +82,29 @@ class API(object): req.path, req.method) try: - responder(req, resp, **params) + # NOTE(kgriffs): Using an inner try..except in order to + # address the case when err_handler raises HTTPError. + # + # NOTE(kgriffs): Coverage is giving false negatives, + # so disabled on relevant lines. All paths are tested + # afaict. + try: + responder(req, resp, **params) # pragma: no cover + except Exception as ex: + for err_type, err_handler in self._error_handlers: + if isinstance(ex, err_type): + err_handler(ex, req, resp, params) + break # pragma: no cover + + else: + # PERF(kgriffs): This will propagate HTTPError to + # the handler below. It makes handling HTTPError + # less efficient, but that is OK since error cases + # don't need to be as fast as the happy path, and + # indeed, should perhaps be slower to create + # backpressure on clients that are issuing bad + # requests. + raise except HTTPError as ex: resp.status = ex.status @@ -92,14 +114,6 @@ class API(object): if req.client_accepts('application/json'): resp.body = ex.json() - except Exception as ex: - for err_type, err_handler in self._error_handlers: - if isinstance(ex, err_type): - err_handler(ex, req, resp, params) - break - else: - raise - # # Set status and headers # diff --git a/falcon/tests/test_error_handlers.py b/falcon/tests/test_error_handlers.py index 87d9eb1..7538cad 100644 --- a/falcon/tests/test_error_handlers.py +++ b/falcon/tests/test_error_handlers.py @@ -1,3 +1,5 @@ +import json + import falcon import falcon.testing as testing @@ -17,7 +19,15 @@ class CustomBaseException(Exception): class CustomException(CustomBaseException): - pass + + @staticmethod + def handle(ex, req, resp, params): + raise falcon.HTTPError( + falcon.HTTP_792, + u'Internet crashed!', + u'Catastrophic weather event', + href=u'http://example.com/api/inconvenient-truth', + href_text=u'Drill, baby drill!') class ErroredClassResource(object): @@ -53,6 +63,23 @@ class TestErrorHandler(testing.TestBase): self.assertRaises(Exception, self.simulate_request, self.test_route) + def test_uncaught_error_else(self): + self.api.add_route(self.test_route, ErroredClassResource()) + + self.assertRaises(Exception, + self.simulate_request, self.test_route) + + def test_converted_error(self): + self.api.add_error_handler(CustomException, CustomException.handle) + + self.api.add_route(self.test_route, ErroredClassResource()) + + body = self.simulate_request(self.test_route, method='DELETE') + self.assertEqual(falcon.HTTP_792, self.srmock.status) + + info = json.loads(body[0].decode()) + self.assertEqual('Internet crashed!', info['title']) + def test_subclass_error(self): self.api.add_error_handler(CustomBaseException, capture_error) diff --git a/falcon/tests/test_example.py b/falcon/tests/test_example.py index 69be12d..39bf221 100644 --- a/falcon/tests/test_example.py +++ b/falcon/tests/test_example.py @@ -1,5 +1,6 @@ import json import logging +from wsgiref import simple_server import falcon @@ -9,7 +10,14 @@ class StorageEngine: class StorageError(Exception): - pass + @staticmethod + def handle(ex, req, resp, params): + description = ('Sorry, couldn\'t write your thing to the ' + 'database. It worked on my box.') + + raise falcon.HTTPError(falcon.HTTP_725, + 'Database Error', + description) def token_is_valid(token, user_id): @@ -21,36 +29,40 @@ def auth(req, resp, params): token = req.get_header('X-Auth-Token') if token is None: + description = ('Please provide an auth token ' + 'as part of the request.') + raise falcon.HTTPUnauthorized('Auth token required', - 'Please provide an auth token ' - 'as part of the request', - 'http://docs.example.com/auth') + description, + scheme=None, + href='http://docs.example.com/auth') if not token_is_valid(token, params['user_id']): + description = ('The provided auth token is not valid. ' + 'Please request a new token and try again.') + raise falcon.HTTPUnauthorized('Authentication required', - 'The provided auth token is ' - 'not valid. Please request a ' - 'new token and try again.', - 'http://docs.example.com/auth') + description, + scheme=None, + href='http://docs.example.com/auth') def check_media_type(req, resp, params): - if not req.client_accepts_json(): + if not req.client_accepts_json: raise falcon.HTTPUnsupportedMediaType( - 'Media Type not Supported', 'This API only supports the JSON media type.', - 'http://docs.examples.com/api/json') + href='http://docs.examples.com/api/json') class ThingsResource: def __init__(self, db): self.db = db - self.logger = logging.getLogger('thingsapi.' + __name__) + self.logger = logging.getLogger('thingsapp.' + __name__) def on_get(self, req, resp, user_id): - marker = req.get_param('marker', default='') - limit = req.get_param('limit', default=50) + marker = req.get_param('marker') or '' + limit = req.get_param_as_int('limit') or 50 try: result = self.db.get_things(marker, limit) @@ -61,7 +73,10 @@ class ThingsResource: 'be back as soon as we fight them off. ' 'We appreciate your patience.') - raise falcon.HTTPServiceUnavailable('Service Outage', description) + raise falcon.HTTPServiceUnavailable( + 'Service Outage', + description, + 30) resp.set_header('X-Powered-By', 'Donuts') resp.status = falcon.HTTP_200 @@ -84,20 +99,23 @@ class ThingsResource: 'Could not decode the request body. The ' 'JSON was incorrect.') - try: - proper_thing = self.db.add_thing(thing) - - except StorageError: - raise falcon.HTTPError(falcon.HTTP_725, - 'Database Error', - "Sorry, couldn't write your thing to the " - 'database. It worked on my machine.') + proper_thing = self.db.add_thing(thing) resp.status = falcon.HTTP_201 resp.location = '/%s/things/%s' % (user_id, proper_thing.id) -wsgi_app = api = falcon.API(before=[auth, check_media_type]) +# Configure your WSGI server to load "things.app" (app is a WSGI callable) +app = falcon.API(before=[auth, check_media_type]) db = StorageEngine() things = ThingsResource(db) -api.add_route('/{user_id}/things', things) +app.add_route('/{user_id}/things', things) + +# If a responder ever raised an instance of StorageError, pass control to +# the given handler. +app.add_error_handler(StorageError, StorageError.handle) + +# Useful for debugging problems in your API; works with pdb.set_trace() +if __name__ == '__main__': + httpd = simple_server.make_server('127.0.0.1', 8000, app) + httpd.serve_forever()