I am creating an API using Flask and Flask-RESTful. I wish to parse the body of an incoming request using flask_restful.reqparse.RequestParser()
. Upon receiving an incorrect JSON, I would like to return a 400 Bad Request response. However, my application is instead returning a 500 Internal Server Error response. I thought the RequestParser()
was supposed to handle these responses automatically? Can anyone explain what is going wrong?
Below is the code for the API Resource
from flask_restful import Resource, reqparse
class Foo(Resource):
parser = reqparse.RequestParser()
parser.add_argument("foo",
type=int,
required=True,
action='append',
help="Request body must contain a 'foo' key which comprises a list of IDs, e.g. {'foo': [44, 3213, 532, 4312]}"
)
def get(self):
data = self.parser.parse_args(strict=True)
return {'bar': data['foo']}
When I send a GET request to the API with the body {"err": [3, 4, 1]}
I receive the following 500 Internal Server Error response:
{"message": "Internal Server Error"}
and not the message I specified in the help
parameter. In my logs I also get the following error message:
KeyError: "'foo'"
I know I could wrap the data = self.parser.parse_args(strict=True)
in a try/except KeyError clause and handle the incorrect JSON myself, but I thought that Flask-RESTful would do that for me? What else could I try?
CodePudding user response:
By defining an APIArgument
class that will be passed to the RequestParser
constructor you can define your own customized response. You also need to pass the bundle_errors = True
to the constructor and configure flask by setting the application configuration key "BUNDLE_ERRORS" to True
See error handling of Request Parsing.
import json
from flask import Flask, Response, abort
from flask_restful import Api, Resource, reqparse
from flask_restful.reqparse import Argument
app = Flask(__name__)
app.config["BUNDLE_ERRORS"] = True
api = Api(app)
class APIArgument(Argument):
def __init__(self, *args, **kwargs):
super(APIArgument, self).__init__(*args, **kwargs)
def handle_validation_error(self, error, bundle_errors):
help_str = "(%s) " % self.help if self.help else ""
msg = "[%s]: %s%s" % (self.name, help_str, str(error))
res = Response(
json.dumps({"message": msg, "code": 400, "status": "FAIL"}),
mimetype="application/json",
status=400,
)
return abort(res)
class Foo(Resource):
parser = reqparse.RequestParser(argument_class=APIArgument, bundle_errors=True)
parser.add_argument(
"foo",
type=int,
action="append",
required=True,
help="Request body must contain a 'foo' key which comprises a list of IDs, e.g. {'foo': [44, 3213, 532, 4312]}",
)
def get(self):
data = self.parser.parse_args(strict=True)
return {'bar': data['foo']}
api.add_resource(Foo, '/')
if __name__ == "__main__":
app.run(port=9000, debug=True)
CodePudding user response:
For those who do not want to deal with the boilerplate code, this also works:
By moving the parser code into the get() route, the default handle_validation_error function provided by flask-restful will be used (I guess):
class Foo(Resource):
def get(self):
parser = reqparse.RequestParser()
parser.add_argument('foo', required=True, action="append", help="Request body must contain a 'foo' key which comprises a list of IDs, e.g. {'foo': [44, 3213, 532, 4312]}",
data = parser.parse_args()
print(data.get("foo")
# this code won't be reached, because the parser aborts the request early if foo is missing
return {'bar': data['foo']}
this means that you have to provide a separate parser for every method, but I think this is usually a good idea anyway, rather than defining this in the route itself, for all methods. flask-restful probably didn't intend to do that this way, that's why it doesn't work out of the box.
Also: Keep in mind that GET requests cannot have a json body if the request comes from a browser, this is only possible if no browser is involved! I generally advise against this, because it's not standard-compatible.