Merge pull request #502 from mcmasterathl/mcmaster/feature_414
feat(api) Add HTTPStatus exception for immediate response
This commit is contained in:
@@ -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
45
falcon/http_status.py
Normal 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
196
tests/test_httpstatus.py
Normal 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')
|
||||
Reference in New Issue
Block a user