Skip to content

Commit

Permalink
Flask-RESTful will fail when Flask deprecates propagate_exceptions: F…
Browse files Browse the repository at this point in the history
…ix (#962)

* Flask's propagate_execptions will be deprecated and Flask-RESTful has a dependency: fix.

* Change a docstring.

* Reorganize imports.

* Get rid of debug assertTrue calls.

* Comment about default value of propagate_exceptions..

* Remove debug and testing from conditional.

* Use debug and testing for sensible return values.

---------

Co-authored-by: aaron <aaron.peters@zolontech.com>
  • Loading branch information
transreductionist and aaron committed May 21, 2023
1 parent b2d9443 commit b52b188
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 8 deletions.
38 changes: 30 additions & 8 deletions flask_restful/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
except ImportError:
from collections import Mapping

_PROPAGATE_EXCEPTIONS = 'PROPAGATE_EXCEPTIONS'

__all__ = ('Api', 'Resource', 'marshal', 'marshal_with', 'marshal_with_field', 'abort')

Expand All @@ -26,14 +27,40 @@ def abort(http_status_code, **kwargs):
"""Raise a HTTPException for the given http_status_code. Attach any keyword
arguments to the exception for later processing.
"""
#noinspection PyUnresolvedReferences
# noinspection PyUnresolvedReferences
try:
original_flask_abort(http_status_code)
except HTTPException as e:
if len(kwargs):
e.data = kwargs
raise


def _get_propagate_exceptions_bool(app):
"""Handle Flask's propagate_exceptions.
If propagate_exceptions is set to True then the exceptions are re-raised rather than being handled
by the app’s error handlers.
The default value for Flask's app.config['PROPAGATE_EXCEPTIONS'] is None. In this case return a sensible
value: self.testing or self.debug.
"""
propagate_exceptions = app.config.get(_PROPAGATE_EXCEPTIONS, False)
if propagate_exceptions is None:
return app.testing or app.debug
return propagate_exceptions


def _handle_flask_propagate_exceptions_config(app, e):
propagate_exceptions = _get_propagate_exceptions_bool(app)
if not isinstance(e, HTTPException) and propagate_exceptions:
exc_type, exc_value, tb = sys.exc_info()
if exc_value is e:
raise
else:
raise e


DEFAULT_REPRESENTATIONS = [('application/json', output_json)]


Expand Down Expand Up @@ -280,19 +307,14 @@ def handle_error(self, e):
"""
got_request_exception.send(current_app._get_current_object(), exception=e)

if not isinstance(e, HTTPException) and current_app.propagate_exceptions:
exc_type, exc_value, tb = sys.exc_info()
if exc_value is e:
raise
else:
raise e
_handle_flask_propagate_exceptions_config(current_app, e)

headers = Headers()
if isinstance(e, HTTPException):
if e.response is not None:
# If HTTPException is initialized with a response, then return e.get_response().
# This prevents specified error response from being overridden.
# eg. HTTPException(response=Response("Hello World"))
# e.g., HTTPException(response=Response("Hello World"))
resp = e.get_response()
return resp

Expand Down
34 changes: 34 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@
from json import dumps, loads, JSONEncoder
from nose.tools import assert_equal # you need it for tests in form of continuations
import six
from types import SimpleNamespace
from unittest.mock import patch

_FLASK_RESTFUL_SYS_EXC_INFO = 'flask_restful.sys.exc_info'
_PROPAGATE_EXCEPTIONS = 'PROPAGATE_EXCEPTIONS'
_APP_ENDPOINT = '/foo'


def setup_propagate_exceptions(propagate_exceptions):
app = Flask(__name__)
app.config[_PROPAGATE_EXCEPTIONS] = propagate_exceptions
api = flask_restful.Api(app)
return SimpleNamespace(app=app, api=api)


def check_unpack(expected, value):
Expand Down Expand Up @@ -117,6 +130,27 @@ def test_handle_error_does_not_swallow_exceptions(self):
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.get_data(), b'{"message": "x"}\n')

@patch(_FLASK_RESTFUL_SYS_EXC_INFO)
def test_handle_error_propagate_exceptions_raise_exception(self, mock_sys_exc_info):
setup = setup_propagate_exceptions(True)
mock_sys_exc_info.return_value = (KeyError, ValueError, Exception.__traceback__)
with setup.app.test_request_context(_APP_ENDPOINT):
self.assertRaises(KeyError, setup.api.handle_error, KeyError)

@patch(_FLASK_RESTFUL_SYS_EXC_INFO)
def test_handle_error_propagate_exceptions_raise(self, mock_sys_exc_info):
setup = setup_propagate_exceptions(True)
mock_sys_exc_info.return_value = (KeyError, ValueError, Exception.__traceback__)
with setup.app.test_request_context(_APP_ENDPOINT):
self.assertRaises(Exception, setup.api.handle_error, ValueError)

@patch(_FLASK_RESTFUL_SYS_EXC_INFO)
def test_handle_error_propagate_exceptions_none(self, mock_sys_exc_info):
setup = setup_propagate_exceptions(None)
mock_sys_exc_info.return_value = (KeyError, ValueError, Exception.__traceback__)
setup.app.debug = True
with setup.app.test_request_context(_APP_ENDPOINT):
self.assertRaises(Exception, setup.api.handle_error, ValueError)

def test_handle_error_does_not_swallow_custom_exceptions(self):
app = Flask(__name__)
Expand Down

0 comments on commit b52b188

Please sign in to comment.