Connexion: Custom validation error handler

Created on 9 Jan 2018  ·  16Comments  ·  Source: zalando/connexion

Description

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, ) and the Flask @app.errorhandler(400), but neither catches the error. I'm sure I'm missing something in the documentation, but could use a pointer/suggestion about how to do what I'm trying to do.

Thanks in advance,
Doug

Version Information

Python - Python 2.7.5
Connexion - Version: 1.1.10.1

question

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:

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

All 16 comments

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:

  1. 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.

  2. 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

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},
)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sharkguto picture sharkguto  ·  5Comments

rudyces picture rudyces  ·  3Comments

writeson picture writeson  ·  4Comments

zd0 picture zd0  ·  4Comments

bioslikk picture bioslikk  ·  4Comments