Using reqparse.add_argument() with default location parameter to parse requests with Content-Type other than "application/json" raises an exception when using Werkzeug>=2.1.0
Author: dlindquistCreated Jun 9, 2023Updated Apr 6, 2024
Background
Werkzeug 2.1.0 changelog:
Request.get_json() will raise a 400 BadRequest error if the Content-Type header is not application/json. This makes a very common source of confusion more visible. :issue:
2339
Werkzeug 2.3.0 changelog:
Request.get_json() will raise a 415 Unsupported Media Type error if the Content-Type header is not application/json, instead of a generic 400. :issue:
2550
Issue
It seems that reqparse.add_argument() with a default location parameter will attempt to load JSON data regardless of the request Content-Type header. This causes Werkzeug to raise a Bad Request or Unsupported Media Type exception if a request is made with a Content-Type header that is not application/json.
Reproduction
Flask==2.3.2
Werkzeug==2.3.6
Flask-RESTful==0.3.10from flask import Flask
from flask_restful import reqparse
def test_json_argument_with_form_location():
'''
passed
'''
app = Flask(__name__)
parser = reqparse.RequestParser()
parser.add_argument("example_argument", location='form')
with app.test_request_context('/', method="post",
data={'example_argument': 'example_value'}):
args = parser.parse_args()
assert args['example_argument'] == 'example_value'
def test_argument_with_json_data():
'''
passed
'''
app = Flask(__name__)
parser = reqparse.RequestParser()
parser.add_argument("example_argument")
with app.test_request_context('/',
json={'example_argument': 'example_value'}):
args = parser.parse_args()
assert args['example_argument'] == 'example_value'
def test_argument_without_json_data():
'''
failed
'''
app = Flask(__name__)
parser = reqparse.RequestParser()
parser.add_argument("example_argument")
with app.test_request_context('/', method="post",
data={'example_argument': 'example_value'}):
args = parser.parse_args()
assert args['example_argument'] == 'example_value'$ pytest
======================================================================== test session starts ========================================================================
platform darwin -- Python 3.8.16, pytest-7.3.1, pluggy-1.0.0
collected 3 items
test_json_argument_without_json_content_type.py ..F [100%]
============================================================================= FAILURES ==============================================================================
___________________________________________________________ test_json_argument_without_json_content_type ____________________________________________________________
def test_json_argument_without_json_content_type():
'''
failed
'''
app = Flask(__name__)
parser = reqparse.RequestParser()
parser.add_argument("example_argument")
with app.test_request_context('/', method="post",
data={'example_argument': 'example_value'}):
> args = parser.parse_args()
test_json_argument_without_json_content_type.py:43:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
env/lib/python3.8/site-packages/flask_restful/reqparse.py:328: in parse_args
value, found = arg.parse(req, self.bundle_errors)
env/lib/python3.8/site-packages/flask_restful/reqparse.py:184: in parse
source = self.source(request)
env/lib/python3.8/site-packages/flask_restful/reqparse.py:125: in source
value = getattr(request, l, None)
env/lib/python3.8/site-packages/werkzeug/wrappers/request.py:561: in json
return self.get_json()
env/lib/python3.8/site-packages/werkzeug/wrappers/request.py:607: in get_json
return self.on_json_loading_failed(None)
env/lib/python3.8/site-packages/flask/wrappers.py:130: in on_json_loading_failed
return super().on_json_loading_failed(e)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <Request 'http://localhost/' [POST]>, e = None
def on_json_loading_failed(self, e: ValueError | None) -> t.Any:
"""Called if :meth:`get_json` fails and isn't silenced.
If this method returns a value, it is used as the return value
for :meth:`get_json`. The default implementation raises
:exc:`~werkzeug.exceptions.BadRequest`.
:param e: If parsing failed, this is the exception. It will be
``None`` if the content type wasn't ``application/json``.
.. versionchanged:: 2.3
Raise a 415 error instead of 400.
"""
if e is not None:
raise BadRequest(f"Failed to decode JSON object: {e}")
> raise UnsupportedMediaType(
"Did not attempt to load JSON data because the request"
" Content-Type was not 'application/json'."
)
E werkzeug.exceptions.UnsupportedMediaType: 415 Unsupported Media Type: Did not attempt to load JSON data because the request Content-Type was not 'application/json'.
env/lib/python3.8/site-packages/werkzeug/wrappers/request.py:650: UnsupportedMediaType
====================================================================== short test summary info ======================================================================
FAILED test_json_argument_without_json_content_type.py::test_json_argument_without_json_content_type - werkzeug.exceptions.UnsupportedMediaType: 415 Unsupported Media Type: Did not attempt to load JSON data because the request Content-Type was not 'application/js...
==================================================================== 1 failed, 2 passed in 0.10s ====================================================================Source: flask-restful/flask-restful