Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Make error initializers more consistent #1655

Merged
merged 29 commits into from Feb 15, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4d6873d
refactor(errors.py): unify init signature of the error classes
CaselIT Jan 25, 2020
137cee8
refactor(errors.py): remove NoRepresentation, HTTPRangeNotSatisfiable…
CaselIT Jan 25, 2020
2ac519f
refactor(errors.py): use py3 style super
CaselIT Jan 25, 2020
7bab132
test(errors): add tests to changes in errors, update other tests
CaselIT Jan 25, 2020
0ffb44b
chore(news): add news fragment for changelog
CaselIT Jan 25, 2020
c98c7f3
docs: remove references to NoRepresentation in documentation
CaselIT Jan 25, 2020
ea554a3
Merge branch 'master' into fix-777-errors
kgriffs Jan 26, 2020
8a95c65
Merge branch 'master' into fix-777-errors
vytas7 Jan 26, 2020
569cabf
refactor(errors): make the arguments kwonly
CaselIT Jan 26, 2020
6695d36
refactor(errors): ensure that the arguments of the error override the…
CaselIT Jan 26, 2020
a48e8c5
docs(errors): update error documentation adding kw arguments
CaselIT Jan 26, 2020
a55f940
refactor(error): use kw args when creating errors
CaselIT Jan 26, 2020
5e04469
chore(cython): fix errors on cython
CaselIT Jan 26, 2020
055cb04
refactor(errors): make the arguments kwonly in httperror
CaselIT Jan 26, 2020
992a2b3
docs(errors): update error documentation adding kw arguments to httpe…
CaselIT Jan 26, 2020
f3aefc4
chore(pep): fix pep errors
CaselIT Jan 26, 2020
2fef4e4
docs(news): update new for changelog
CaselIT Jan 26, 2020
c0b2ec6
fix(errors): allow a list of two tuples as the headers of the errors …
CaselIT Jan 29, 2020
af547cd
docs(error): add note about headers
CaselIT Feb 1, 2020
f1d1765
test(httperror): add missing test
CaselIT Feb 1, 2020
4109f27
Merge branch 'master' into fix-777-errors
CaselIT Feb 4, 2020
6969e0c
refactor(errors): deprecate positional arguments instead of making th…
CaselIT Feb 4, 2020
89e137a
style(pep8): fix pep8 errors
CaselIT Feb 5, 2020
e875d15
chore: add missing tests and address review
CaselIT Feb 5, 2020
ab3645a
docs: be polite
CaselIT Feb 5, 2020
c13bd66
docs(changelog): update changelog
CaselIT Feb 8, 2020
9fef3b3
docs(changelog): address review comments
CaselIT Feb 11, 2020
769de42
Merge branch 'master' into fix-777-errors
kgriffs Feb 12, 2020
8793055
Merge branch 'master' into fix-777-errors
kgriffs Feb 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/_newsfragments/777-refactor-errors.feature.rst
@@ -0,0 +1,2 @@
Update to the error classes in ``falcon.errors`` to have ``title`` and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs just a slight rewording to be consistent with other fragments:

The error classes in ``falcon.errors`` were updated to have...

``description`` keyword arguments
2 changes: 2 additions & 0 deletions docs/_newsfragments/777-refactor-errors.removal.rst
@@ -0,0 +1,2 @@
The class :class:`~.falcon.http_error.NoRepresentation` was removed since
all :class:`~.falcon.HTTPError` have optional representation
2 changes: 1 addition & 1 deletion docs/api/errors.rst
Expand Up @@ -79,7 +79,7 @@ Base Class
Mixins
------

.. autoclass:: falcon.http_error.NoRepresentation
.. autoclass:: falcon.http_error.OptionalRepresentation
:members:

.. _predefined_errors:
Expand Down
3 changes: 2 additions & 1 deletion falcon/app.py
Expand Up @@ -729,7 +729,8 @@ def set_error_serializer(self, serializer):
The default serializer will not render any response body for
:class:`~.HTTPError` instances where the `has_representation`
property evaluates to ``False`` (such as in the case of types
that subclass :class:`falcon.http_error.NoRepresentation`).
that subclass :class:`falcon.http_error.OptionalRepresentation`
and do not provide a description).
However a custom serializer will be called regardless of the
property value, and it may choose to override the
representation logic.
Expand Down
190 changes: 73 additions & 117 deletions falcon/errors.py

Large diffs are not rendered by default.

28 changes: 2 additions & 26 deletions falcon/http_error.py
Expand Up @@ -79,7 +79,7 @@ class HTTPError(Exception):
returns ``True``, but child classes may override it
in order to return ``False`` when an empty HTTP body is desired.

(See also: :class:`falcon.http_error.NoRepresentation`)
(See also: :class:`falcon.http_error.OptionalRepresentation`)

Note:
A custom error serializer
Expand Down Expand Up @@ -203,30 +203,6 @@ def to_xml(self):
et.tostring(error_element, encoding='utf-8'))


class NoRepresentation:
"""Mixin for ``HTTPError`` child classes that have no representation.

This class can be mixed in when inheriting from ``HTTPError``, in order
to override the `has_representation` property such that it always
returns ``False``. This, in turn, will cause Falcon to return an empty
response body to the client.

You can use this mixin when defining errors that either should not have
a body (as dictated by HTTP standards or common practice), or in the
case that a detailed error response may leak information to an attacker.

Note:
This mixin class must appear before ``HTTPError`` in the base class
list when defining the child; otherwise, it will not override the
`has_representation` property as expected.

"""

@property
def has_representation(self):
return False


class OptionalRepresentation:
"""Mixin for ``HTTPError`` child classes that may have a representation.

Expand All @@ -247,4 +223,4 @@ class OptionalRepresentation:
"""
@property
def has_representation(self):
return super(OptionalRepresentation, self).description is not None
return super().description is not None
114 changes: 64 additions & 50 deletions tests/test_error.py
@@ -1,17 +1,24 @@
import pytest

import falcon
import falcon.errors as errors
import falcon.status_codes as status


@pytest.mark.parametrize('err, title', [
(falcon.HTTPBadRequest, status.HTTP_400),
(falcon.HTTPUnauthorized, status.HTTP_401),
(falcon.HTTPForbidden, status.HTTP_403),
(falcon.HTTPNotFound, status.HTTP_404),
(errors.HTTPRouteNotFound, status.HTTP_404),
(falcon.HTTPNotAcceptable, status.HTTP_406),
(falcon.HTTPConflict, status.HTTP_409),
(falcon.HTTPGone, status.HTTP_410),
(falcon.HTTPLengthRequired, status.HTTP_411),
(falcon.HTTPPreconditionFailed, status.HTTP_412),
(falcon.HTTPPayloadTooLarge, status.HTTP_413),
(falcon.HTTPUriTooLong, status.HTTP_414),
(falcon.HTTPUnsupportedMediaType, status.HTTP_415),
(falcon.HTTPUnprocessableEntity, status.HTTP_422),
(falcon.HTTPLocked, status.HTTP_423),
(falcon.HTTPFailedDependency, status.HTTP_424),
Expand Down Expand Up @@ -40,17 +47,40 @@ def test_with_default_title_and_desc(err, title):
assert 'Retry-After' not in e.value.headers


@pytest.mark.parametrize('err, title, args', (
(falcon.HTTPMethodNotAllowed, status.HTTP_405, (['GET'], )),
(falcon.HTTPRangeNotSatisfiable, status.HTTP_416, (11,)),
))
def test_with_default_title_and_desc_args(err, title, args):
with pytest.raises(err) as e:
raise err(*args)

assert e.value.title == title
assert e.value.description is None

if e.value.headers:
assert 'Retry-After' not in e.value.headers


@pytest.mark.parametrize('err', [
falcon.HTTPBadRequest,
falcon.HTTPUnauthorized,
falcon.HTTPForbidden,
falcon.HTTPNotFound,
errors.HTTPRouteNotFound,
falcon.HTTPNotAcceptable,
falcon.HTTPConflict,
falcon.HTTPGone,
falcon.HTTPLengthRequired,
falcon.HTTPPreconditionFailed,
falcon.HTTPPreconditionRequired,
falcon.HTTPPayloadTooLarge,
falcon.HTTPUriTooLong,
falcon.HTTPUnsupportedMediaType,
falcon.HTTPUnprocessableEntity,
falcon.HTTPLocked,
falcon.HTTPFailedDependency,
falcon.HTTPPreconditionRequired,
falcon.HTTPTooManyRequests,
falcon.HTTPRequestHeaderFieldsTooLarge,
falcon.HTTPUnavailableForLegalReasons,
falcon.HTTPInternalServerError,
Expand All @@ -76,6 +106,23 @@ def test_with_title_desc_and_headers(err):
assert e.value.headers['foo'] == 'bar'


@pytest.mark.parametrize('err, args', (
(falcon.HTTPMethodNotAllowed, (['GET'], )),
(falcon.HTTPRangeNotSatisfiable, (11,)),
))
def test_with_title_desc_and_headers_args(err, args):
title = 'trace'
desc = 'boom'
headers = {'foo': 'bar'}

with pytest.raises(err) as e:
raise err(*args, title=title, description=desc, headers=headers)

assert e.value.title == title
assert e.value.description == desc
assert e.value.headers['foo'] == 'bar'


@pytest.mark.parametrize('err', [
falcon.HTTPServiceUnavailable,
falcon.HTTPTooManyRequests,
Expand All @@ -101,13 +148,6 @@ def test_with_retry_after_and_headers(err):
assert e.value.headers['foo'] == 'bar'


def test_http_range_not_satisfiable_with_headers():
try:
raise falcon.HTTPRangeNotSatisfiable(resource_length=0, headers={'foo': 'bar'})
except falcon.HTTPRangeNotSatisfiable as e:
assert e.headers['foo'] == 'bar'


def test_http_unauthorized_no_title_and_desc_and_challenges_and_headers():
try:
raise falcon.HTTPUnauthorized()
Expand All @@ -132,49 +172,23 @@ def test_http_unauthorized_with_title_and_desc_and_challenges_and_headers():
assert 'bar' == e.headers['foo']


def test_http_not_acceptable_no_title_and_desc_and_challenges():
try:
raise falcon.HTTPNotAcceptable()
except falcon.HTTPNotAcceptable as e:
assert e.description is None


def test_http_not_acceptable_with_title():
try:
raise falcon.HTTPNotAcceptable(title='test title')
except falcon.HTTPNotAcceptable as e:
assert e.title == 'test title'


def test_http_not_acceptable_with_title_and_desc_and_challenges():
try:
raise falcon.HTTPNotAcceptable(description='Testdescription')
except falcon.HTTPNotAcceptable as e:
assert 'Testdescription' == e.description


def test_http_unsupported_media_type_no_title_and_desc_and_challenges():
try:
raise falcon.HTTPUnsupportedMediaType()
except falcon.HTTPUnsupportedMediaType as e:
assert e.description is None


def test_http_unsupported_media_type_with_title():
try:
raise falcon.HTTPUnsupportedMediaType(title='test title')
except falcon.HTTPUnsupportedMediaType as e:
assert e.title == 'test title'


def test_http_unsupported_media_type_with_title_and_desc_and_challenges():
try:
raise falcon.HTTPUnsupportedMediaType(description='boom')
except falcon.HTTPUnsupportedMediaType as e:
assert e.description == 'boom'


def test_http_error_repr():
error = falcon.HTTPBadRequest()
_repr = '<%s: %s>' % (error.__class__.__name__, error.status)
assert error.__repr__() == _repr


@pytest.mark.parametrize('err, args, title, desc', (
(falcon.HTTPInvalidHeader, ('foo', 'bar'), 'Invalid header value',
'The value provided for the "bar" header is invalid. foo'),
(falcon.HTTPMissingHeader, ('foo',), 'Missing header value', 'The "foo" header is required.'),
(falcon.HTTPInvalidParam, ('foo', 'bar'), 'Invalid parameter',
'The "bar" parameter is invalid. foo'),
(falcon.HTTPMissingParam, ('foo',), 'Missing parameter', 'The "foo" parameter is required.'),
))
def test_custom_400(err, args, title, desc):
with pytest.raises(err) as e:
raise err(*args)

assert e.value.title == title
assert e.value.description == desc
2 changes: 1 addition & 1 deletion tests/test_headers.py
Expand Up @@ -349,7 +349,7 @@ def test_required_header(self, client):
except falcon.HTTPMissingHeader as ex:
assert isinstance(ex, falcon.HTTPBadRequest)
assert ex.title == 'Missing header value'
expected_desc = 'The X-Not-Found header is required.'
expected_desc = 'The "X-Not-Found" header is required.'
assert ex.description == expected_desc

@pytest.mark.parametrize('status', (falcon.HTTP_204, falcon.HTTP_304))
Expand Down
4 changes: 2 additions & 2 deletions tests/test_httperror.py
Expand Up @@ -782,7 +782,7 @@ def test_invalid_header(self, client):
client.app.add_route('/400', InvalidHeaderResource())
response = client.simulate_request(path='/400')

expected_desc = ('The value provided for the X-Auth-Token '
expected_desc = ('The value provided for the "X-Auth-Token" '
'header is invalid. Please provide a valid token.')

expected_body = {
Expand All @@ -800,7 +800,7 @@ def test_missing_header(self, client):

expected_body = {
'title': 'Missing header value',
'description': 'The X-Auth-Token header is required.',
'description': 'The "X-Auth-Token" header is required.',
}

assert response.status == falcon.HTTP_400
Expand Down
14 changes: 7 additions & 7 deletions tests/test_request_attrs.py
Expand Up @@ -520,7 +520,7 @@ def test_range_invalid(self, asgi):
req.range

headers = {'Range': 'bytes=-'}
expected_desc = ('The value provided for the Range header is '
expected_desc = ('The value provided for the "Range" header is '
'invalid. The range offsets are missing.')
self._test_error_details(headers, 'range',
falcon.HTTPInvalidHeader,
Expand Down Expand Up @@ -583,7 +583,7 @@ def test_range_invalid(self, asgi):
req.range

headers = {'Range': 'bytes=x-y'}
expected_desc = ('The value provided for the Range header is '
expected_desc = ('The value provided for the "Range" header is '
'invalid. It must be a range formatted '
'according to RFC 7233.')
self._test_error_details(headers, 'range',
Expand All @@ -592,7 +592,7 @@ def test_range_invalid(self, asgi):
asgi)

headers = {'Range': 'bytes=0-0,-1'}
expected_desc = ('The value provided for the Range '
expected_desc = ('The value provided for the "Range" '
'header is invalid. The value must be a '
'continuous range.')
self._test_error_details(headers, 'range',
Expand All @@ -601,7 +601,7 @@ def test_range_invalid(self, asgi):
asgi)

headers = {'Range': '10-'}
expected_desc = ('The value provided for the Range '
expected_desc = ('The value provided for the "Range" '
'header is invalid. The value must be '
"prefixed with a range unit, e.g. 'bytes='")
self._test_error_details(headers, 'range',
Expand All @@ -628,7 +628,7 @@ def test_content_length(self, asgi):
def test_bogus_content_length_nan(self, asgi):
headers = {'content-length': 'fuzzy-bunnies'}
expected_desc = ('The value provided for the '
'Content-Length header is invalid. The value '
'"Content-Length" header is invalid. The value '
'of the header must be a number.')
self._test_error_details(headers, 'content_length',
falcon.HTTPInvalidHeader,
Expand All @@ -637,7 +637,7 @@ def test_bogus_content_length_nan(self, asgi):

def test_bogus_content_length_neg(self, asgi):
headers = {'content-length': '-1'}
expected_desc = ('The value provided for the Content-Length '
expected_desc = ('The value provided for the "Content-Length" '
'header is invalid. The value of the header '
'must be a positive number.')
self._test_error_details(headers, 'content_length',
Expand Down Expand Up @@ -667,7 +667,7 @@ def test_date_invalid(self, asgi, header, attr):

# Date formats don't conform to RFC 1123
headers = {header: 'Thu, 04 Apr 2013'}
expected_desc = ('The value provided for the {} '
expected_desc = ('The value provided for the "{}" '
'header is invalid. It must be formatted '
'according to RFC 7231, Section 7.1.1.1')

Expand Down