fix(HTTPError): Remove dependency on blank title to determine serialization

This patch attempts to make the code easier to understand and maintain
going forward by removing the dependency between the title attribute and
the "serializable" property. The "serializable" property was renamed to
be more indicitive of the context in which it is actually used.

Closes #265
This commit is contained in:
Kurt Griffiths
2014-10-16 18:22:57 -05:00
parent d01af249b2
commit 4be86d1a60
5 changed files with 53 additions and 46 deletions

View File

@@ -371,7 +371,7 @@ class API(object):
if error.headers is not None: if error.headers is not None:
resp.set_headers(error.headers) resp.set_headers(error.headers)
if error.serializable: if error.has_representation:
media_type, body = self._serialize_error(req, error) media_type, body = self._serialize_error(req, error)
if body is not None: if body is not None:

View File

@@ -94,7 +94,11 @@ class HTTPNotFound(HTTPError):
""" """
def __init__(self): def __init__(self):
HTTPError.__init__(self, status.HTTP_404, None, None) HTTPError.__init__(self, status.HTTP_404)
@property
def has_representation(self):
return False
class HTTPMethodNotAllowed(HTTPError): class HTTPMethodNotAllowed(HTTPError):
@@ -116,21 +120,27 @@ class HTTPMethodNotAllowed(HTTPError):
HTTPError.__init__(self, status.HTTP_405, 'Method not allowed', HTTPError.__init__(self, status.HTTP_405, 'Method not allowed',
**kwargs) **kwargs)
# NOTE(kgriffs): Trigger an empty body in the response; 405
# responses don't usually have bodies, so we only send one
# if the caller indicates they want one by providing some
# details in the kwargs.
if kwargs: if kwargs:
title = 'Method not allowed' title = 'Method not allowed'
self._has_representation = True
else: else:
# NOTE(kgriffs): Trigger an empty body in the response; 405
# responses don't usually have bodies, so we only send one
# if the caller indicates they want one, by way of specifying
# a description, href, and/or other details.
title = None title = None
self._has_representation = False
# NOTE(kgriffs): Inject the "Allow" header so it will be included # NOTE(kgriffs): Inject the "Allow" header so it will be included
# in the HTTP response. # in the HTTP response.
headers = kwargs.setdefault('headers', {}) headers = kwargs.setdefault('headers', {})
headers['Allow'] = ', '.join(allowed_methods) headers['Allow'] = ', '.join(allowed_methods)
HTTPError.__init__(self, status.HTTP_405, title, **kwargs) HTTPError.__init__(self, status.HTTP_405, title=title, **kwargs)
@property
def has_representation(self):
return self._has_representation
class HTTPNotAcceptable(HTTPError): class HTTPNotAcceptable(HTTPError):
@@ -262,7 +272,11 @@ class HTTPRangeNotSatisfiable(HTTPError):
def __init__(self, resource_length): def __init__(self, resource_length):
headers = {'Content-Range': 'bytes */' + str(resource_length)} headers = {'Content-Range': 'bytes */' + str(resource_length)}
HTTPError.__init__(self, status.HTTP_416, None, headers=headers) HTTPError.__init__(self, status.HTTP_416, headers=headers)
@property
def has_representation(self):
return False
class HTTPInternalServerError(HTTPError): class HTTPInternalServerError(HTTPError):

View File

@@ -35,14 +35,11 @@ class HTTPError(Exception):
Attributes: Attributes:
status (str): HTTP status line, such as "748 Confounded by Ponies". status (str): HTTP status line, such as "748 Confounded by Ponies".
serializable (bool): Returns *True* IFF the error should be has_representation (bool): Read-only property that determines
serialized when composing the HTTP response. whether error details will be serialized when composing
the HTTP response. In ``HTTPError`` this property always
Note: returns ``True``, but child classes may override this property
If an app sets a custom error serializer, it will only in order to return ``False`` when an empty HTTP body is desired.
be called when the error's `serializable` property is
``True``.
title (str): Error title to send to the client. Will be ``None`` if title (str): Error title to send to the client. Will be ``None`` if
the error should result in an HTTP response with an empty body. the error should result in an HTTP response with an empty body.
description (str): Description of the error to send to the client. description (str): Description of the error to send to the client.
@@ -54,19 +51,9 @@ class HTTPError(Exception):
Args: Args:
status (str): HTTP status code and text, such as "400 Bad Request" status (str): HTTP status code and text, such as "400 Bad Request"
title (str): Human-friendly error title. Set to ``None`` if you wish
Falcon to return an empty response body (all remaining args will
be ignored except for headers.) This will set the error's
`serializable` property to ``False``.
Note:
Set `title` to ``None`` when you don't wish to disclose
sensitive information about why a request was refused,
when the status and headers are self-descriptive, or when
the HTTP specification forbids returning a body for the
status code in question.
Keyword Args: Keyword Args:
title (str): Human-friendly error title (default ``None``).
description (str): Human-friendly description of the error, along with description (str): Human-friendly description of the error, along with
a helpful suggestion or two (default ``None``). a helpful suggestion or two (default ``None``).
headers (dict or list): A dictionary of header names and values headers (dict or list): A dictionary of header names and values
@@ -95,7 +82,7 @@ class HTTPError(Exception):
for this error"). for this error").
code (int): An internal code that customers can reference in their code (int): An internal code that customers can reference in their
support request or to help them when searching for knowledge support request or to help them when searching for knowledge
base articles related to this error. base articles related to this error (default ``None``).
""" """
__slots__ = ( __slots__ = (
@@ -104,7 +91,7 @@ class HTTPError(Exception):
'description', 'description',
'headers', 'headers',
'link', 'link',
'code' 'code',
) )
def __init__(self, status, title=None, description=None, headers=None, def __init__(self, status, title=None, description=None, headers=None,
@@ -124,8 +111,8 @@ class HTTPError(Exception):
self.link = None self.link = None
@property @property
def serializable(self): def has_representation(self):
return self.title is not None return True
def raw(self, obj_type=dict): def raw(self, obj_type=dict):
"""Returns a raw dictionary representing the error. """Returns a raw dictionary representing the error.
@@ -142,18 +129,20 @@ class HTTPError(Exception):
""" """
assert self.serializable assert self.has_representation
obj = obj_type() obj = obj_type()
obj['title'] = self.title
if self.description: if self.title is not None:
obj['title'] = self.title
if self.description is not None:
obj['description'] = self.description obj['description'] = self.description
if self.code: if self.code is not None:
obj['code'] = self.code obj['code'] = self.code
if self.link: if self.link is not None:
obj['link'] = self.link obj['link'] = self.link
return obj return obj
@@ -178,18 +167,20 @@ class HTTPError(Exception):
""" """
assert self.serializable assert self.has_representation
error_element = et.Element('error') error_element = et.Element('error')
et.SubElement(error_element, 'title').text = self.title
if self.description: if self.title is not None:
et.SubElement(error_element, 'title').text = self.title
if self.description is not None:
et.SubElement(error_element, 'description').text = self.description et.SubElement(error_element, 'description').text = self.description
if self.code: if self.code is not None:
et.SubElement(error_element, 'code').text = str(self.code) et.SubElement(error_element, 'code').text = str(self.code)
if self.link: if self.link is not None:
link_element = et.SubElement(error_element, 'link') link_element = et.SubElement(error_element, 'link')
for key in ('text', 'href', 'rel'): for key in ('text', 'href', 'rel'):

View File

@@ -5,7 +5,7 @@ import falcon.testing as testing
def validate_output(req, resp): def validate_output(req, resp):
raise falcon.HTTPError(falcon.HTTP_723, title=None) raise falcon.HTTPError(falcon.HTTP_723, 'Tricky')
def serialize_body(req, resp): def serialize_body(req, resp):
@@ -257,7 +257,9 @@ class TestHooks(testing.TestBase):
def test_output_validator(self): def test_output_validator(self):
self.simulate_request(self.test_route) self.simulate_request(self.test_route)
self.assertEqual(falcon.HTTP_723, self.srmock.status) self.assertEqual(falcon.HTTP_723, self.srmock.status)
self.assertEqual(None, self.resource.resp.body_encoded)
expected = b'{\n "title": "Tricky"\n}'
self.assertEqual(expected, self.resource.resp.body_encoded)
def test_serializer(self): def test_serializer(self):
self.simulate_request(self.test_route, method='PUT') self.simulate_request(self.test_route, method='PUT')

View File

@@ -37,7 +37,7 @@ class FaultyResource:
code=8733224) code=8733224)
def on_patch(self, req, resp): def on_patch(self, req, resp):
raise falcon.HTTPError(falcon.HTTP_400, 'No-can-do') raise falcon.HTTPError(falcon.HTTP_400)
class UnicodeFaultyResource(object): class UnicodeFaultyResource(object):
@@ -166,7 +166,7 @@ class TestHTTPError(testing.TestBase):
def test_no_description_json(self): def test_no_description_json(self):
body = self.simulate_request('/fail', method='PATCH') body = self.simulate_request('/fail', method='PATCH')
self.assertEqual(self.srmock.status, falcon.HTTP_400) self.assertEqual(self.srmock.status, falcon.HTTP_400)
self.assertEqual(body, [b'{\n "title": "No-can-do"\n}']) self.assertEqual(body, [b'{}'])
def test_no_description_xml(self): def test_no_description_xml(self):
body = self.simulate_request('/fail', method='PATCH', body = self.simulate_request('/fail', method='PATCH',
@@ -174,7 +174,7 @@ class TestHTTPError(testing.TestBase):
self.assertEqual(self.srmock.status, falcon.HTTP_400) self.assertEqual(self.srmock.status, falcon.HTTP_400)
expected_xml = (b'<?xml version="1.0" encoding="UTF-8"?>' expected_xml = (b'<?xml version="1.0" encoding="UTF-8"?>'
b'<error><title>No-can-do</title></error>') b'<error />')
self.assertEqual(body, [expected_xml]) self.assertEqual(body, [expected_xml])