Skip to content

Commit

Permalink
Merge pull request #502 from mcmasterathl/mcmaster/feature_414
Browse files Browse the repository at this point in the history
feat(api) Add HTTPStatus exception for immediate response
  • Loading branch information
kgriffs committed Apr 15, 2015
2 parents 0c6edb6 + 34e89db commit d836d65
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 4 deletions.
23 changes: 19 additions & 4 deletions falcon/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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
Expand Down Expand Up @@ -211,6 +212,11 @@ def __call__(self, env, start_response):
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)
Expand Down Expand Up @@ -483,13 +489,22 @@ def _get_responder(self, req):

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)
Expand Down
45 changes: 45 additions & 0 deletions falcon/http_status.py
Original file line number Diff line number Diff line change
@@ -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 changes: 196 additions & 0 deletions tests/test_httpstatus.py
Original file line number Diff line number Diff line change
@@ -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')

0 comments on commit d836d65

Please sign in to comment.