spec-first / connexion

Connexion is a modern Python web framework that makes spec-first and api-first development easy.
https://connexion.readthedocs.io/en/latest/
Apache License 2.0
4.47k stars 761 forks source link

Support connexion.Problem as an exception #366

Closed rafaelcaricio closed 7 years ago

rafaelcaricio commented 7 years ago

Description

Sometimes you are creating API code that needs to just stop and return an error to the client (also rollback db changes in case it happens). A standard way of achieving that is using exceptions. Connexion provides support for standard problem responses for APIs (https://tools.ietf.org/html/rfc7807) which is great. We currently support that by providing the connexion.problem function that can be used by our operation handlers to return problems to clients.

Currently if we want to use exceptions to break execution you need to create a internal exception for your API and then using a decorator turn the exception in a problem response to be returned as the operation handler response.

Would be nice if Connexion would offer a connexion.Problem exception that would be automatically handled by Connexion itself to return problem responses.

Expected behaviour

from connexion import Problem

def my_operation_handler(param_1):
    some_internal_work()
    return 'Cool stuff!'

def some_internal_work():
    if not you_are_really_allowed():
        raise Problem(403, 'Oh noes', 'I cannot allow you to perform this action')
    ...

Current behaviour

You can mimic this by doing:


import connexion
from decorator import decorator

class Problem(Exception):
    def __init__(self, status_code, title, description):
        self.status_code = status_code
        self.title = title
        self.description = description

@decorator
def handle_problem_exceptions(func: callable, *args, **kwargs):
    try:
        return func(*args, **kwargs)
    except Problem as prb:
        return connexion.problem(prb.status_code, prb.title, prb.description)

@handle_problem_exceptions
def my_operation_handler(param_1):
    some_internal_work()
    return 'Cool stuff!'

def some_internal_work():
    if not you_are_really_allowed():
        raise Problem(403, 'Oh noes', 'I cannot allow you to perform this action')
wilsonge commented 7 years ago

For this to work with the flask exception handler, we need the new problem exception to subtype werkzeug.exceptions.HTTPException

The question is for the 403/401 do we want to have separate exceptions that allow us to trigger on the individual exception type. For example it would be nice to hav different renders based on the whether it's a param validation issue, and oauth issue and response validation issue etc.

rafaelcaricio commented 7 years ago

@wilsonge Agree. I like the idea of different renders for different kind of problems. We could do a better job here and return something like:

   {
   "type": "https://example.net/validation-error",
   "title": "Your request parameters didn't validate.",
   "invalid-params": [ {
                         "name": "age",
                         "reason": "must be a positive integer"
                       },
                       {
                         "name": "color",
                         "reason": "must be 'green', 'red' or 'blue'"}
                     ]
   }

When the parameter is invalid for example.

rafaelcaricio commented 7 years ago

BTW, this example is from the RFC https://tools.ietf.org/html/rfc7807

wilsonge commented 7 years ago

https://github.com/zalando/connexion/compare/master...wilsonge:oauth_exception?expand=1 This is the kinda thing I was thinking of?

I think it would be nicer if there was a title property too - but it depends if that's b/c or not. We probably want some sort of multi-inheritance for that if we go that way?

wilsonge commented 7 years ago

How are we going to determine nice messages for invalid parameters. Finding what is invalid is decently straightforward. Assembling human friendly reasons seems less so at the moment :/

rafaelcaricio commented 7 years ago

@wilsonge I see. Maybe you can came up with a bit more friendly than the current? The current version of this message is pretty bad. You cannot easily extract what was wrong. Anyway, thank you for taking the time to research how to do it.