diff --git a/CHANGES b/CHANGES index 8311e2e7..91a31040 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Version 0.9 Relase date to be decided, codename to be chosen. +- The :func:`flask.Request.on_json_loading_failed` now returns a JSON formatted + response by default. - The :func:`flask.url_for` function now can generate anchors to the generated links. - The :func:`flask.url_for` function now can also explicitly generate diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 89c6c188..e48f8dc3 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -40,6 +40,18 @@ class JSONTestCase(FlaskTestCase): rv = c.post('/json', data='malformed', content_type='application/json') self.assert_equal(rv.status_code, 400) + def test_json_bad_requests_content_type(self): + app = flask.Flask(__name__) + @app.route('/json', methods=['POST']) + def return_json(): + return unicode(flask.request.json) + c = app.test_client() + rv = c.post('/json', data='malformed', content_type='application/json') + self.assert_equal(rv.status_code, 400) + self.assert_equal(rv.mimetype, 'application/json') + self.assert_('description' in flask.json.loads(rv.data)) + self.assert_('

' not in flask.json.loads(rv.data)['description']) + def test_json_body_encoding(self): app = flask.Flask(__name__) app.testing = True diff --git a/flask/wrappers.py b/flask/wrappers.py index f6ec2788..3df697f7 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -10,7 +10,7 @@ """ from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase -from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import BadRequest, HTTPException from werkzeug.utils import cached_property from .debughelpers import attach_enctype_error_multidict @@ -18,6 +18,43 @@ from .helpers import json, _assert_have_json from .globals import _request_ctx_stack +class JSONHTTPException(HTTPException): + """A base class for HTTP exceptions with ``Content-Type: + application/json``. + + The ``description`` attribute of this class must set to a string (*not* an + HTML string) which describes the error. + + """ + + def get_body(self, environ): + """Overrides :meth:`werkzeug.exceptions.HTTPException.get_body` to + return the description of this error in JSON format instead of HTML. + + """ + return json.dumps(dict(description=self.get_description(environ))) + + def get_headers(self, environ): + """Returns a list of headers including ``Content-Type: + application/json``. + + """ + return [('Content-Type', 'application/json')] + + +class JSONBadRequest(JSONHTTPException, BadRequest): + """Represents an HTTP ``400 Bad Request`` error whose body contains an + error message in JSON format instead of HTML format (as in the superclass). + + """ + + #: The description of the error which occurred as a string. + description = ( + 'The browser (or proxy) sent a request that this server could not ' + 'understand.' + ) + + class Request(RequestBase): """The request object used by default in Flask. Remembers the matched endpoint and view arguments. @@ -108,12 +145,23 @@ class Request(RequestBase): def on_json_loading_failed(self, e): """Called if decoding of the JSON data failed. The return value of - this method is used by :attr:`json` when an error ocurred. The - default implementation raises a :class:`~werkzeug.exceptions.BadRequest`. + this method is used by :attr:`json` when an error ocurred. The default + implementation raises a :class:`JSONBadRequest`, which is a subclass of + :class:`~werkzeug.exceptions.BadRequest` which sets the + ``Content-Type`` to ``application/json`` and provides a JSON-formatted + error description:: + + {"description": "The browser (or proxy) sent a request that \ + this server could not understand."} + + .. versionchanged:: 0.9 + + Return a :class:`JSONBadRequest` instead of a + :class:`~werkzeug.exceptions.BadRequest` by default. .. versionadded:: 0.8 """ - raise BadRequest() + raise JSONBadRequest() def _load_form_data(self): RequestBase._load_form_data(self)