feature(api): Error handlers may raise HTTPError
Error handlers may now raise instances of HTTPError and expect them to be converted to a proper HTTP response, as they would be when raised from within a regular resource responder or hook. Also added examples to the README of using the error handler feature.
This commit is contained in:
47
README.md
47
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()
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
47
README.rst
47
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
|
||||
|
||||
@@ -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
|
||||
#
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user