I am using the flask_expects_json library to validate the JSON schema in API requests.
This is working, however, when a request is not valid, the error is thrown in the logs but the details of the error message are not returned by the API. The response is just a generic message "Internal Server Error".
Error message:
{"event": "Exception on /ingest [PUT]", "logger": "app", "level": "error", "exception": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.8/site-packages/flask_expects_json/__init__.py\", line 30, in decorated_function\n validate(data, schema)\n File \"/usr/local/lib/python3.8/site-packages/jsonschema/validators.py\", line 934, in validate\n raise error\njsonschema.exceptions.ValidationError: 'location' is a required property\n\nFailed validating 'required' in schema}
Response:
{
"message": "Internal Server Error"
}
Question: How do I handle the errors from flask_expects_json and return the details of the error instead of the server error? In other words, I need the details of the error in the response, such as:
{
"error": "'location' is a required property"
}
Here is the code I have so far.
from flask import Flask, request, jsonify, current_app
from flask_restx import Resource, Api
from flask_expects_json import expects_json
def create_app():
app = Flask(__name__)
api = Api(app, version='1.0', title='Test API', description='Test API Description')
@api.route('/ingest', endpoint="ingest", methods=['GET', 'POST', 'PUT', 'DELETE'])
class Ingest(Resource):
@expects_json(test_schema)
def get(self, *args, **kwargs):
# logic removed for brevity
result = get_result()
return result
return app
if __name__ == '__main__':
app.run()
I also tried writing an error handler to catch these errors as per the flask documentation on error handling and other similar Stack Overflow questions but it does not appear to be doing anything.
class BadRequest(Exception):
status_code = 400
def __init__(self, message, status_code=None, payload=None):
Exception.__init__(self)
self.message = message
if status_code is not None:
self.status_code = status_code
self.payload = payload
def to_dict(self):
rv = dict(self.payload or ())
rv['message'] = self.message
return rv
@app.errorhandler(BadRequest)
def handle_bad_request(error):
response = jsonify(error.to_dict())
response.status_code = error.status_code
return response
UPDATE: This error is probably caused by the gunicorn structlog configuration that is attempting to convert a ValidationError to JSON and failing therefore throwing the 500 error.
Here is the gunicorn.conf.py
import structlog
import os
from datetime import datetime
if os.environ.get('ENV') == 'development':
reload = True
hostname = os.uname().nodename
timestamp = datetime.today().strftime('%Y-%m-%d')
pre_chain = [
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.processors.TimeStamper(fmt='iso', utc=True),
]
logconfig_dict = {
"version": 1,
"disable_existing_loggers": True,
"formatters": {
"json_formatter": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.processors.JSONRenderer(),
"foreign_pre_chain": pre_chain,
}
},
"handlers": {
"error_console": {
"class": "logging.FileHandler",
"formatter": "json_formatter",
"filename": "/home/logs/error_console_{}_{}.log".format(hostname, timestamp),
"mode": "a"
},
"console": {
"class": "logging.FileHandler",
"formatter": "json_formatter",
"filename": "/home/logs/console_{}_{}.log".format(hostname, timestamp),
"mode": "a"
}
},
"loggers": {
'gunicorn.error': {
'handlers': ['console'],
'level': os.environ.get('APP_LOG_LEVEL', 'INFO'),
'propagate': False,
},
'gunicorn.access': {
'handlers': ['console'],
'level': os.environ.get('APP_LOG_LEVEL', 'INFO'),
'propagate': False,
}
}
}
Here is the 500 error that is occurring when it attempts to throw the 400 Bad Request error but then runs into a TypeError. TypeError: Object of type ValidationError is not JSON serializable
.
Failed validating 'required' in schema
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/local/lib/python3.8/json/__init__.py", line 231, in dumps
return _default_encoder.encode(obj)
File "/usr/local/lib/python3.8/json/encoder.py", line 199, in encode
chunks = self.iterencode(o, _one_shot=True)
File "/usr/local/lib/python3.8/json/encoder.py", line 257, in iterencode
return _iterencode(o, 0)
File "/usr/local/lib/python3.8/json/encoder.py", line 179, in default
raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type ValidationError is not JSON serializable
UPDATE 2 [WORKAROUND]: As mentioned I think this issue is due to an error handling issue when expects_json throws a ValidationError. It might be related to the gunicorn structlog config or it might be something that can be fixed in the expects_json library. As a workaround I removed expects_json and created a custom decorator to validate the json schema using the jsonschema library similar to the accepted answer in this related question. Flask: Decorator to verify JSON and JSON Schema
Here is the full code of my solution. Again this does not actually solve the issue directly but is an acceptable workaround and does provide a bit more control.
from flask import request, jsonify, make_response, current_app
import os, json, jsonschema
def validate_json_schema(schema_name):
def decorator(f):
def wrapper(*args, **kwargs):
basedir = current_app.config['BASE_DIR']
jsonmodelsdir = basedir + '/jsonmodels'
if not request.json:
error_msg = "Missing JSON data."
return make_response(jsonify({'error': 'invalidRequest', 'message': error_msg}), 400)
try:
with open('{}/{}.json'.format(jsonmodelsdir, schema_name)) as json_file:
json_model = json.load(json_file)
except Exception as e:
error_msg = "Server error: Unable to get json model."
current_app.logger.error(error_msg)
current_app.logger.error(e)
return make_response(jsonify({'error': 'unavailable', 'message': error_msg}), 500)
try:
jsonschema.validate(request.json, json_model)
except json.decoder.JSONDecodeError as e:
error_msg = "Invalid JSON format: {}".format(e)
return make_response(jsonify({'error': 'invalidRequest', 'message': error_msg}), 400)
except jsonschema.exceptions.ValidationError as e:
error_msg = "Invalid JSON schema: {}".format(e)
return make_response(jsonify({'error': 'invalidRequest', 'message': error_msg}), 400)
except Exception as e:
error_msg = "Server error: Unable to validate json model."
current_app.logger.error(e)
return make_response(jsonify({'error': 'unavailable', 'message': error_msg}), 500)
return f(*args, **kwargs)
return wrapper
return decorator