I need some help about how to catch and provide a custom handler for validation errors. This is not an error in the validation itself, but a legit problem with an expected parameter that's misspelled. This generates the connexion response:
{
"detail": "<details about the problem>"
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
This is fine, but I'd like to catch this and reformat the above data into a message structure my application is expecting. I've tried using add_error_handler(400,
Thanks in advance,
Doug
Python - Python 2.7.5
Connexion - Version: 1.1.10.1
Hi,
You can add the custom error handler message in app (returned from connexion.App). We did custom validation as below:
from jsonschema import ValidationError
app.add_error_handler(ValidationError, bad_request_error_handler)
def bad_request_error_handler(error):
db.session.rollback()
response = connexion.problem(
status=HTTP_400_BAD_REQUEST,
title='Bad request',
detail=error.message or error.__class__.__name__,
)
return response.flask_response_object()
if Validationerror is thrown it will call bad_request_error_handler where you can define the customer problem object with the values you want.
Hi,
Thanks for the response, very much appreciated. However, it didn't work for me, here's a section of my code:
import connexion
from jsonschema import ValidationError
def bad_request_error_handler(error):
v = 1
def create_app(config_key, instance_config=None):
# create the connexion instance
connex_app = connexion.FlaskApp(__name__,
specification_dir='./files/swagger/')
connex_app.server = 'gevent'
# get the Flask app instance
app = connex_app.app
connex_app.add_error_handler(ValidationError, bad_request_error_handler)
With this is place I changed my swagger.yml file so the validation would fail (changed a parameter enum value so one of the expected values would no longer match). Running the code that sends the parameter that no longer matches the enum doesn't hit the bad_request_error_handler() function, it just does it's normal error handling and sends this response:
{
"detail": "<details about response>",
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
Doug
Hi Doug,
Connexion eventually uses jsonschema for validation of request body.
The validate_schema
function in class RequestBodyValidator
in https://github.com/zalando/connexion/blob/master/connexion/decorators/validation.py handles request body validation, and further calls jsonschema's validator. If the jsonschme's validator.validate
method throws ValidationError exception it handles that in the except clause. If you override the validate_schema
function somewhere in your project and remove the try andexcept clause, your bad_request_error_handler will catch the ValidationError. To do this you have to add validator_map
in your connexion.FlaskApp definition (http://connexion.readthedocs.io/en/latest/request.html some info is provided on validator_map here)
Below is a sample code for the same
app = connexion.App(__name__, specification_dir='swagger/', validator_map={'body': RequestBodyValidator})
And define this RequestBodyValidator somewhere in your project as below:
import connexion
from connexion import decorators
class RequestBodyValidator(decorators.validation.RequestBodyValidator):
def validate_schema(self, data):
if self.is_null_value_valid and connexion.utils.is_null(data):
return None
self.validator.validate(data)
The RequestBodyValidator above is same as one in connexion's RequestBodyValidator. The only difference is that we have removed try and catch so that ValidationError is caught by bad_request_error_handler instead.
@LeenaBhegade does it make sense to change Connexion's RequestBodyValidator?
I would like to use parameters through functions like add_error_handler instead (and not override any classes provided by connexion) to get it working but if I could not find a straight forward way of doing it. Do you think there could be an alternative way to solve it ? I would be happy to implement it.
I would also prefer to enhance the error reporting from my app when validation errors occur, and would prefer not to override Connexion classes. I'm also willing to work to contribute something, but would prefer guidance on what would be acceptable. Some potential approaches:
Enhance the error message when RequestBodyValidator handles the exception. My thought would be to use the ErrorTree to provide both the reason for the failure(s) and the reason(s) they failed, like the example "print(tree[0].errors["type"].message)" in the jsonschema error handling guide. No API change, but the string provided in 'detail' would likely be significantly longer if multiple errors exist; could be capped at the first 1 or 2 if this is an issue.
Add a configuration value that takes a lambda for handling validation errors
Please let me know if either of these could be accepted as a change.
Hi Leena,
Thanks for your response and the clarification about the RequestBodyValidator. I modified my code to reflect what you wrote, but still no luck, I can't get the system to call my 'bad_request_error_handler()' for a validation error. Here's a simplified version of my code:
import connexion
from connexion import decorators
from jsonschema import ValidationError
class RequestBodyValidator(decorators.validation.RequestBodyValidator):
def validate_schema(self, data):
if self.is_null_value_valid and connexion.utils.is_null(data):
return None
self.validator.validate(data)
def bad_request_error_handler(error):
v = 1
def create_app(config_key, instance_config=None):
# create the connexion instance
connex_app = connexion.FlaskApp(__name__,
specification_dir='./files/swagger/',
validator_map={
'body': RequestBodyValidator
})
connex_app.add_error_handler(ValidationError, bad_request_error_handler)
Again, thanks for your help with this.
Doug
Answer is here: https://github.com/zalando/connexion/issues/138
I ran into a similar issue when I tried to add a custom handler for query/form parameter validation errors. Here, adding a handler for connexion.exceptions.ExtraParameterProblem
(rather than jsonschema.ValidationError
) did the trick. Perhaps this will be of help to someone.
Example:
from connexion.exceptions import ExtraParameterProblem
def handle_bad_request():
# Do what you like
app.add_error_handler(ExtraParameterProblem, handle_bad_request)
@LeenaBhegade
Clarification in documentation or a patch is needed.
add_api()
has no keyword arg validator_map
so I suspect it falls into "old style options". If the documented approach (in user manual) is deprecated as "old style", then please tell us what is the current and proper way to do this.
thanks
marc
I handle this elegantly by defining an error middleware. This way you process all errors (connexion validation and downstream errors) from a single handler:
import json
from http import HTTPStatus
from aiohttp import web
from aiohttp.web import json_response
@web.middleware
async def error_middleware(request: web.Request, handler: Callable) -> web.Response:
try:
response = await handler(request)
if response.status >= 400:
# connexion validation errors
error_message = json.loads(response.body)
error = Exception(error_message['detail'])
return process_error(error, response.status)
return response
except Exception as error:
# other exceptions
return process_error(error, HTTPStatus.INTERNAL_SERVER_ERROR)
def process_error(error, status):
log.exception('{}: {}'.format(type(error), error))
response = CommonResponse(errors=['{}: {}'.format(type(error), error)])
return json_response(response.to_dict(), status=status)
Sample response
{
"txnId": "e3cd8f2e-a33b-434c-b5b8-2969602ee9f1",
"datetime": "2019-02-25 11:50:00",
"errors": [
"<class 'Exception'>: 'bad-value' is not one of ['yearly', 'quarterly', 'monthly']"
]
}
With minor changes, you can make it work for your specific app setup.
there were some changes to the api, since this issue was created.
this is what worked for me:
import connexion
from connexion.problem import problem
from connexion import decorators
from jsonschema import ValidationError
class RequestBodyValidator(decorators.validation.RequestBodyValidator):
"""
This class overrides the default connexion RequestBodyValidator
so that it returns the complete string representation of the
error, rather than just returning the error message.
For more information:
- https://github.com/zalando/connexion/issues/558
- https://connexion.readthedocs.io/en/latest/request.html
"""
def validate_schema(self, data, url):
if self.is_null_value_valid and is_null(data):
return None
try:
self.validator.validate(data)
except ValidationError as exception:
logger.error(
"{url} validation error: {error}".format(url=url, error=exception),
extra={"validator": "body"},
)
return problem(400, "Bad Request", str(exception))
return None
app = connexion.App(__name__)
app.add_api(
"app.yml",
strict_validation=True,
validate_responses=True,
validator_map={"body": RequestBodyValidator},
)
@simoami in your case how do you add the middleware in a AioHttp app? If I have:
app = connexion.AioHttpApp(
__name__, port=2000, specification_dir="../",
)
Where do I set a middleware? Looking at the docs here https://docs.aiohttp.org/en/stable/web_advanced.html#middlewares I did not find where to set something like
app = web.Application(middlewares=[middleware_1,
middleware_2])
on Conexion.
@fvcaputo It's been a while since I looked at this. Here's is how my code looks right now:
src/middleware.py
from aiohttp import web
import typing
from typing import Callable, Coroutine
AIO_MIDDLEWARE = Callable[[web.Request, AIO_HANDLER], Coroutine[typing.Any, typing.Any, web.Response]]
@web.middleware
async def error_middleware(request: web.Request, handler: Callable) -> web.Response:
...
}
// other middlewares defined here
def get() -> typing.List[AIO_MIDDLEWARE]:
return [error_middleware, some_other_middleware]
app.py
from aiohttp import web
import connexion
from src import middleware
connexion_app = connexion.AioHttpApp(__name__,
specification_dir='swagger',
only_one_api=True,
options={
'middlewares': middleware.get(),
},
)
...
def main():
logger.info('App started')
connexion_app.run(port=int(config.port))
if __name__ == '__main__':
main()
I have yet to update my code to the latest api changes.
Thanks @simoami that seems to be exactly what I need!
@writeson did you finally get it working? This is what worked for me finally.. return problem() does not work for some reason. we have to raise an exception.
class CustomRequestBodyValidator(RequestBodyValidator):
def validate_schema(self, data, url):
if self.is_null_value_valid and is_null(data):
return None
try:
self.validator.validate(data)
except ValidationError as exception:
LOG.error(
"{url} validation error: {error}".format(url=url, error=exception),
extra={"validator": "body"},
)
raise BadRequestProblem("Bad Request test custom message", str(exception))
return None
app = connexion.App(__name__)
app.add_api(
"app.yaml",
strict_validation=True,
validate_responses=True,
validator_map={"body": CustomRequestBodyValidator},
)
Most helpful comment
Hi Leena,
Thanks for your response and the clarification about the RequestBodyValidator. I modified my code to reflect what you wrote, but still no luck, I can't get the system to call my 'bad_request_error_handler()' for a validation error. Here's a simplified version of my code:
Again, thanks for your help with this.
Doug