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.

Ref: https://github.com/flask-restful/flask-restful/blob/88cce53a8cd65830bf1815185a42ba24e5db78c6/flask_restful/reqparse.py#L127

Reproduction

Flask==2.3.2
Werkzeug==2.3.6
Flask-RESTful==0.3.10
python
from 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