Browse Source

Merge pull request #2348 from davidism/bad-request

make debugging bad key errors easier
pull/2136/merge
David Lord 8 years ago committed by GitHub
parent
commit
9049755e3f
  1. 6
      CHANGES
  2. 5
      docs/config.rst
  3. 31
      flask/app.py
  4. 7
      tests/test_basic.py
  5. 2
      tests/test_helpers.py

6
CHANGES

@ -49,7 +49,7 @@ Major release, unreleased
work with the ``flask`` command. If they take a single parameter or a work with the ``flask`` command. If they take a single parameter or a
parameter named ``script_info``, the ``ScriptInfo`` object will be passed. parameter named ``script_info``, the ``ScriptInfo`` object will be passed.
(`#2319`_) (`#2319`_)
- FLASK_APP=myproject.app:create_app('dev') support. - FLASK_APP=myproject.app:create_app('dev') support.
- ``FLASK_APP`` can be set to an app factory, with arguments if needed, for - ``FLASK_APP`` can be set to an app factory, with arguments if needed, for
example ``FLASK_APP=myproject.app:create_app('dev')``. (`#2326`_) example ``FLASK_APP=myproject.app:create_app('dev')``. (`#2326`_)
- ``View.provide_automatic_options = True`` is set on the view function from - ``View.provide_automatic_options = True`` is set on the view function from
@ -62,6 +62,9 @@ Major release, unreleased
parameters for use when building base URL. (`#1621`_) parameters for use when building base URL. (`#1621`_)
- Set ``APPLICATION_ROOT = '/'`` by default. This was already the implicit - Set ``APPLICATION_ROOT = '/'`` by default. This was already the implicit
default when it was set to ``None``. default when it was set to ``None``.
- ``TRAP_BAD_REQUEST_ERRORS`` is enabled by default in debug mode.
``BadRequestKeyError`` has a message with the bad key in debug mode instead
of the generic bad request message. (`#2348`_)
.. _#1489: https://github.com/pallets/flask/pull/1489 .. _#1489: https://github.com/pallets/flask/pull/1489
.. _#1621: https://github.com/pallets/flask/pull/1621 .. _#1621: https://github.com/pallets/flask/pull/1621
@ -80,6 +83,7 @@ Major release, unreleased
.. _#2316: https://github.com/pallets/flask/pull/2316 .. _#2316: https://github.com/pallets/flask/pull/2316
.. _#2319: https://github.com/pallets/flask/pull/2319 .. _#2319: https://github.com/pallets/flask/pull/2319
.. _#2326: https://github.com/pallets/flask/pull/2326 .. _#2326: https://github.com/pallets/flask/pull/2326
.. _#2348: https://github.com/pallets/flask/pull/2348
Version 0.12.2 Version 0.12.2
-------------- --------------

5
docs/config.rst

@ -109,9 +109,10 @@ The following configuration values are used internally by Flask:
Trying to access a key that doesn't exist from request dicts like ``args`` Trying to access a key that doesn't exist from request dicts like ``args``
and ``form`` will return a 400 Bad Request error page. Enable this to treat and ``form`` will return a 400 Bad Request error page. Enable this to treat
the error as an unhandled exception instead so that you get the interactive the error as an unhandled exception instead so that you get the interactive
debugger. This is a more specific version of ``TRAP_HTTP_EXCEPTIONS``. debugger. This is a more specific version of ``TRAP_HTTP_EXCEPTIONS``. If
unset, it is enabled in debug mode.
Default: ``False`` Default: ``None``
.. py:data:: SECRET_KEY .. py:data:: SECRET_KEY

31
flask/app.py

@ -18,7 +18,8 @@ from threading import Lock
from werkzeug.datastructures import ImmutableDict, Headers from werkzeug.datastructures import ImmutableDict, Headers
from werkzeug.exceptions import BadRequest, HTTPException, \ from werkzeug.exceptions import BadRequest, HTTPException, \
InternalServerError, MethodNotAllowed, default_exceptions InternalServerError, MethodNotAllowed, default_exceptions, \
BadRequestKeyError
from werkzeug.routing import BuildError, Map, RequestRedirect, Rule from werkzeug.routing import BuildError, Map, RequestRedirect, Rule
from . import cli, json from . import cli, json
@ -317,7 +318,7 @@ class Flask(_PackageBoundObject):
'SESSION_REFRESH_EACH_REQUEST': True, 'SESSION_REFRESH_EACH_REQUEST': True,
'MAX_CONTENT_LENGTH': None, 'MAX_CONTENT_LENGTH': None,
'SEND_FILE_MAX_AGE_DEFAULT': timedelta(hours=12), 'SEND_FILE_MAX_AGE_DEFAULT': timedelta(hours=12),
'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_BAD_REQUEST_ERRORS': None,
'TRAP_HTTP_EXCEPTIONS': False, 'TRAP_HTTP_EXCEPTIONS': False,
'EXPLAIN_TEMPLATE_LOADING': False, 'EXPLAIN_TEMPLATE_LOADING': False,
'PREFERRED_URL_SCHEME': 'http', 'PREFERRED_URL_SCHEME': 'http',
@ -1542,13 +1543,21 @@ class Flask(_PackageBoundObject):
exception is not called and it shows up as regular exception in the exception is not called and it shows up as regular exception in the
traceback. This is helpful for debugging implicitly raised HTTP traceback. This is helpful for debugging implicitly raised HTTP
exceptions. exceptions.
.. versionchanged:: 1.0
Bad request errors are not trapped by default in debug mode.
.. versionadded:: 0.8 .. versionadded:: 0.8
""" """
if self.config['TRAP_HTTP_EXCEPTIONS']: if self.config['TRAP_HTTP_EXCEPTIONS']:
return True return True
if self.config['TRAP_BAD_REQUEST_ERRORS']:
trap_bad_request = self.config['TRAP_BAD_REQUEST_ERRORS']
# if unset, trap based on debug mode
if (trap_bad_request is None and self.debug) or trap_bad_request:
return isinstance(e, BadRequest) return isinstance(e, BadRequest)
return False return False
def handle_user_exception(self, e): def handle_user_exception(self, e):
@ -1559,16 +1568,30 @@ class Flask(_PackageBoundObject):
function will either return a response value or reraise the function will either return a response value or reraise the
exception with the same traceback. exception with the same traceback.
.. versionchanged:: 1.0
Key errors raised from request data like ``form`` show the the bad
key in debug mode rather than a generic bad request message.
.. versionadded:: 0.7 .. versionadded:: 0.7
""" """
exc_type, exc_value, tb = sys.exc_info() exc_type, exc_value, tb = sys.exc_info()
assert exc_value is e assert exc_value is e
# ensure not to trash sys.exc_info() at that point in case someone # ensure not to trash sys.exc_info() at that point in case someone
# wants the traceback preserved in handle_http_exception. Of course # wants the traceback preserved in handle_http_exception. Of course
# we cannot prevent users from trashing it themselves in a custom # we cannot prevent users from trashing it themselves in a custom
# trap_http_exception method so that's their fault then. # trap_http_exception method so that's their fault then.
# MultiDict passes the key to the exception, but that's ignored
# when generating the response message. Set an informative
# description for key errors in debug mode or when trapping errors.
if (
(self.debug or self.config['TRAP_BAD_REQUEST_ERRORS'])
and isinstance(e, BadRequestKeyError)
# only set it if it's still the default description
and e.description is BadRequestKeyError.description
):
e.description = "KeyError: '{0}'".format(*e.args)
if isinstance(e, HTTPException) and not self.trap_http_exception(e): if isinstance(e, HTTPException) and not self.trap_http_exception(e):
return self.handle_http_exception(e) return self.handle_http_exception(e)

7
tests/test_basic.py

@ -975,12 +975,17 @@ def test_trapping_of_bad_request_key_errors(app, client):
def fail(): def fail():
flask.request.form['missing_key'] flask.request.form['missing_key']
assert client.get('/fail').status_code == 400 rv = client.get('/fail')
assert rv.status_code == 400
assert b'missing_key' not in rv.data
app.config['TRAP_BAD_REQUEST_ERRORS'] = True app.config['TRAP_BAD_REQUEST_ERRORS'] = True
with pytest.raises(KeyError) as e: with pytest.raises(KeyError) as e:
client.get("/fail") client.get("/fail")
assert e.errisinstance(BadRequest) assert e.errisinstance(BadRequest)
assert 'missing_key' in e.value.description
def test_trapping_of_all_http_exceptions(app, client): def test_trapping_of_all_http_exceptions(app, client):

2
tests/test_helpers.py

@ -44,6 +44,7 @@ class TestJSON(object):
def test_post_empty_json_adds_exception_to_response_content_in_debug(self, app, client): def test_post_empty_json_adds_exception_to_response_content_in_debug(self, app, client):
app.config['DEBUG'] = True app.config['DEBUG'] = True
app.config['TRAP_BAD_REQUEST_ERRORS'] = False
@app.route('/json', methods=['POST']) @app.route('/json', methods=['POST'])
def post_json(): def post_json():
@ -56,6 +57,7 @@ class TestJSON(object):
def test_post_empty_json_wont_add_exception_to_response_if_no_debug(self, app, client): def test_post_empty_json_wont_add_exception_to_response_if_no_debug(self, app, client):
app.config['DEBUG'] = False app.config['DEBUG'] = False
app.config['TRAP_BAD_REQUEST_ERRORS'] = False
@app.route('/json', methods=['POST']) @app.route('/json', methods=['POST'])
def post_json(): def post_json():

Loading…
Cancel
Save