Merge pull request #502 from mcmasterathl/mcmaster/feature_414

feat(api) Add HTTPStatus exception for immediate response
This commit is contained in:
Kurt Griffiths
2015-04-15 18:46:40 -04:00
3 changed files with 260 additions and 4 deletions

View File

@@ -18,6 +18,7 @@ import six
from falcon import api_helpers as helpers
from falcon import DEFAULT_MEDIA_TYPE
from falcon.http_error import HTTPError
from falcon.http_status import HTTPStatus
from falcon.request import Request, RequestOptions
from falcon.response import Response
import falcon.responders
@@ -211,6 +212,11 @@ class API(object):
self._call_resp_mw(middleware_stack, req, resp, resource)
raise
except HTTPStatus as ex:
self._compose_status_response(req, resp, ex)
self._call_after_hooks(req, resp, resource)
self._call_resp_mw(middleware_stack, req, resp, resource)
except HTTPError as ex:
self._compose_error_response(req, resp, ex)
self._call_after_hooks(req, resp, resource)
@@ -483,13 +489,22 @@ class API(object):
return (responder, params, resource)
def _compose_status_response(self, req, resp, http_status):
"""Composes a response for the given HTTPStatus instance."""
resp.status = http_status.status
if http_status.headers is not None:
resp.set_headers(http_status.headers)
if getattr(http_status, "body", None) is not None:
resp.body = http_status.body
def _compose_error_response(self, req, resp, error):
"""Composes a response for the given HTTPError instance."""
resp.status = error.status
if error.headers is not None:
resp.set_headers(error.headers)
# Use the HTTPStatus handler function to set status/headers
self._compose_status_response(req, resp, error)
if error.has_representation:
media_type, body = self._serialize_error(req, error)

45
falcon/http_status.py Normal file
View File

@@ -0,0 +1,45 @@
# Copyright 2015 by Hurricane Labs LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class HTTPStatus(Exception):
"""Represents a generic HTTP status.
Raise this class from a hook, middleware, or a responder to stop handling
the request and skip to the response handling.
Attributes:
status (str): HTTP status line, e.g. '748 Confounded by Ponies'.
headers (dict): Extra headers to add to the response.
body (str or unicode): String representing response content. If
Unicode, Falcon will encode as UTF-8 in the response.
Args:
status (str): HTTP status code and text, such as
'748 Confounded by Ponies'.
headers (dict): Extra headers to add to the response.
body (str or unicode): String representing response content. If
Unicode, Falcon will encode as UTF-8 in the response.
"""
__slots__ = (
'status',
'headers',
'body'
)
def __init__(self, status, headers=None, body=None):
self.status = status
self.headers = headers
self.body = body

196
tests/test_httpstatus.py Normal file
View File

@@ -0,0 +1,196 @@
# -*- coding: utf-8
import falcon.testing as testing
import falcon
from falcon.http_status import HTTPStatus
def before_hook(req, resp, params):
raise HTTPStatus(falcon.HTTP_200,
headers={"X-Failed": "False"},
body="Pass")
def after_hook(req, resp, resource):
resp.status = falcon.HTTP_200
resp.set_header("X-Failed", "False")
resp.body = "Pass"
def noop_after_hook(req, resp, resource):
pass
class TestStatusResource:
@falcon.before(before_hook)
def on_get(self, req, resp):
resp.status = falcon.HTTP_500
resp.set_header("X-Failed", "True")
resp.body = "Fail"
def on_post(self, req, resp):
resp.status = falcon.HTTP_500
resp.set_header("X-Failed", "True")
resp.body = "Fail"
raise HTTPStatus(falcon.HTTP_200,
headers={"X-Failed": "False"},
body="Pass")
@falcon.after(after_hook)
def on_put(self, req, resp):
resp.status = falcon.HTTP_500
resp.set_header("X-Failed", "True")
resp.body = "Fail"
def on_patch(self, req, resp):
raise HTTPStatus(falcon.HTTP_200,
body=None)
@falcon.after(noop_after_hook)
def on_delete(self, req, resp):
raise HTTPStatus(falcon.HTTP_200,
headers={"X-Failed": "False"},
body="Pass")
class TestHookResource:
def on_get(self, req, resp):
resp.status = falcon.HTTP_500
resp.set_header("X-Failed", "True")
resp.body = "Fail"
def on_patch(self, req, resp):
raise HTTPStatus(falcon.HTTP_200,
body=None)
def on_delete(self, req, resp):
raise HTTPStatus(falcon.HTTP_200,
headers={"X-Failed": "False"},
body="Pass")
class TestHTTPStatus(testing.TestBase):
def before(self):
self.resource = TestStatusResource()
self.api.add_route('/status', self.resource)
def test_raise_status_in_before_hook(self):
""" Make sure we get the 200 raised by before hook """
body = self.simulate_request('/status', method='GET', decode='utf-8')
self.assertEqual(self.srmock.status, falcon.HTTP_200)
self.assertIn(('x-failed', 'False'), self.srmock.headers)
self.assertEqual(body, 'Pass')
def test_raise_status_in_responder(self):
""" Make sure we get the 200 raised by responder """
body = self.simulate_request('/status', method='POST', decode='utf-8')
self.assertEqual(self.srmock.status, falcon.HTTP_200)
self.assertIn(('x-failed', 'False'), self.srmock.headers)
self.assertEqual(body, 'Pass')
def test_raise_status_runs_after_hooks(self):
""" Make sure after hooks still run """
body = self.simulate_request('/status', method='PUT', decode='utf-8')
self.assertEqual(self.srmock.status, falcon.HTTP_200)
self.assertIn(('x-failed', 'False'), self.srmock.headers)
self.assertEqual(body, 'Pass')
def test_raise_status_survives_after_hooks(self):
""" Make sure after hook doesn't overwrite our status """
body = self.simulate_request('/status', method='DELETE',
decode='utf-8')
self.assertEqual(self.srmock.status, falcon.HTTP_200)
self.assertIn(('x-failed', 'False'), self.srmock.headers)
self.assertEqual(body, 'Pass')
def test_raise_status_empty_body(self):
""" Make sure passing None to body results in empty body """
body = self.simulate_request('/status', method='PATCH', decode='utf-8')
self.assertEqual(body, '')
class TestHTTPStatusWithGlobalHooks(testing.TestBase):
def before(self):
self.resource = TestHookResource()
def test_raise_status_in_before_hook(self):
""" Make sure we get the 200 raised by before hook """
self.api = falcon.API(before=[before_hook])
self.api.add_route('/status', self.resource)
body = self.simulate_request('/status', method='GET', decode='utf-8')
self.assertEqual(self.srmock.status, falcon.HTTP_200)
self.assertIn(('x-failed', 'False'), self.srmock.headers)
self.assertEqual(body, 'Pass')
def test_raise_status_runs_after_hooks(self):
""" Make sure we still run after hooks """
self.api = falcon.API(after=[after_hook])
self.api.add_route('/status', self.resource)
body = self.simulate_request('/status', method='GET', decode='utf-8')
self.assertEqual(self.srmock.status, falcon.HTTP_200)
self.assertIn(('x-failed', 'False'), self.srmock.headers)
self.assertEqual(body, 'Pass')
def test_raise_status_survives_after_hooks(self):
""" Make sure after hook doesn't overwrite our status """
self.api = falcon.API(after=[noop_after_hook])
self.api.add_route('/status', self.resource)
body = self.simulate_request('/status', method='DELETE',
decode='utf-8')
self.assertEqual(self.srmock.status, falcon.HTTP_200)
self.assertIn(('x-failed', 'False'), self.srmock.headers)
self.assertEqual(body, 'Pass')
def test_raise_status_in_process_request(self):
""" Make sure we can raise status from middleware process request """
class TestMiddleware:
def process_request(self, req, resp):
raise HTTPStatus(falcon.HTTP_200,
headers={"X-Failed": "False"},
body="Pass")
self.api = falcon.API(middleware=TestMiddleware())
self.api.add_route('/status', self.resource)
body = self.simulate_request('/status', method='GET', decode='utf-8')
self.assertEqual(self.srmock.status, falcon.HTTP_200)
self.assertIn(('x-failed', 'False'), self.srmock.headers)
self.assertEqual(body, 'Pass')
def test_raise_status_in_process_resource(self):
""" Make sure we can raise status from middleware process resource """
class TestMiddleware:
def process_resource(self, req, resp, resource):
raise HTTPStatus(falcon.HTTP_200,
headers={"X-Failed": "False"},
body="Pass")
self.api = falcon.API(middleware=TestMiddleware())
self.api.add_route('/status', self.resource)
body = self.simulate_request('/status', method='GET', decode='utf-8')
self.assertEqual(self.srmock.status, falcon.HTTP_200)
self.assertIn(('x-failed', 'False'), self.srmock.headers)
self.assertEqual(body, 'Pass')
def test_raise_status_runs_process_response(self):
""" Make sure process_response still runs """
class TestMiddleware:
def process_response(self, req, resp, response):
resp.status = falcon.HTTP_200
resp.set_header("X-Failed", "False")
resp.body = "Pass"
self.api = falcon.API(middleware=TestMiddleware())
self.api.add_route('/status', self.resource)
body = self.simulate_request('/status', method='GET', decode='utf-8')
self.assertEqual(self.srmock.status, falcon.HTTP_200)
self.assertIn(('x-failed', 'False'), self.srmock.headers)
self.assertEqual(body, 'Pass')