Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Converting response to raise a ProblemException #955

Merged
merged 9 commits into from
Oct 24, 2019
1 change: 0 additions & 1 deletion connexion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from .exceptions import ProblemException # NOQA
# add operation for backwards compatability
from .operations import compat
from .problem import problem # NOQA
badcure marked this conversation as resolved.
Show resolved Hide resolved
from .resolver import Resolution, Resolver, RestyResolver # NOQA

full_name = '{}.operation'.format(__package__)
Expand Down
7 changes: 4 additions & 3 deletions connexion/apps/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
class AbstractApp(object):
def __init__(self, import_name, api_cls, port=None, specification_dir='',
host=None, server=None, arguments=None, auth_all_paths=False, debug=False,
resolver=None, options=None):
resolver=None, options=None, skip_error_handlers=False):
"""
:param import_name: the name of the application package
:type import_name: str
Expand Down Expand Up @@ -63,8 +63,9 @@ def __init__(self, import_name, api_cls, port=None, specification_dir='',

logger.debug('Specification directory: %s', self.specification_dir)

logger.debug('Setting error handlers')
self.set_errors_handlers()
if not skip_error_handlers:
logger.debug('Setting error handlers')
self.set_errors_handlers()
badcure marked this conversation as resolved.
Show resolved Hide resolved

@abc.abstractmethod
def create_app(self):
Expand Down
5 changes: 4 additions & 1 deletion connexion/apps/flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ def common_error_handler(exception):
:type exception: Exception
"""
if isinstance(exception, ProblemException):
response = exception.to_problem()
response = problem(
status=exception.status, title=exception.title, detail=exception.detail,
type=exception.type, instance=exception.instance, headers=exception.headers,
ext=exception.ext)
else:
if not isinstance(exception, werkzeug.exceptions.HTTPException):
exception = werkzeug.exceptions.InternalServerError()
Expand Down
5 changes: 4 additions & 1 deletion connexion/decorators/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import time

from werkzeug.exceptions import HTTPException

from connexion.exceptions import ProblemException
try:
import uwsgi_metrics
HAS_UWSGI_METRICS = True # pragma: no cover
Expand Down Expand Up @@ -40,6 +40,9 @@ def wrapper(request):
except HTTPException as http_e:
status = http_e.code
raise http_e
except ProblemException as prob_e:
status = prob_e.status
raise prob_e
finally:
end_time_s = time.time()
delta_s = end_time_s - start_time_s
Expand Down
16 changes: 5 additions & 11 deletions connexion/decorators/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from ..exceptions import (NonConformingResponseBody,
NonConformingResponseHeaders)
from ..problem import problem
from ..utils import all_json, has_coroutine
from .decorator import BaseDecorator
from .validation import ResponseBodyValidator
Expand Down Expand Up @@ -86,16 +85,11 @@ def __call__(self, function):
"""

def _wrapper(request, response):
try:
connexion_response = \
self.operation.api.get_connexion_response(response, self.mimetype)
self.validate_response(
connexion_response.body, connexion_response.status_code,
connexion_response.headers, request.url)

except (NonConformingResponseBody, NonConformingResponseHeaders) as e:
response = problem(500, e.reason, e.message)
return self.operation.api.get_response(response)
connexion_response = \
self.operation.api.get_connexion_response(response, self.mimetype)
self.validate_response(
connexion_response.body, connexion_response.status_code,
connexion_response.headers, request.url)

return response

Expand Down
27 changes: 9 additions & 18 deletions connexion/decorators/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
from jsonschema import Draft4Validator, ValidationError, draft4_format_checker
from werkzeug import FileStorage

from ..exceptions import ExtraParameterProblem
from ..exceptions import ExtraParameterProblem, BadRequestProblem, UnsupportedMediaTypeProblem
from ..http_facts import FORM_CONTENT_TYPES
from ..json_schema import Draft4RequestValidator, Draft4ResponseValidator
from ..problem import problem
from ..utils import all_json, boolean, is_json_mimetype, is_null, is_nullable

logger = logging.getLogger('connexion.decorators.validation')
Expand Down Expand Up @@ -126,14 +125,10 @@ def wrapper(request):

if ctype_is_json:
# Content-Type is json but actual body was not parsed
return problem(400,
"Bad Request",
"Request body is not valid JSON"
)
raise BadRequestProblem(detail="Request body is not valid JSON")
else:
# the body has contents that were not parsed as JSON
return problem(415,
"Unsupported Media Type",
raise UnsupportedMediaTypeProblem(
"Invalid Content-type ({content_type}), expected JSON data".format(
content_type=request.headers.get("Content-Type", "")
))
Expand Down Expand Up @@ -163,7 +158,7 @@ def wrapper(request):
errs += [str(e)]
print(errs)
if errs:
return problem(400, 'Bad Request', errs)
raise BadRequestProblem(detail=errs)

error = self.validate_schema(data, request.url)
if error:
Expand All @@ -185,7 +180,7 @@ def validate_schema(self, data, url):
logger.error("{url} validation error: {error}".format(url=url,
error=exception.message),
extra={'validator': 'body'})
return problem(400, 'Bad Request', str(exception.message))
raise BadRequestProblem(detail=str(exception.message))

return None

Expand Down Expand Up @@ -323,26 +318,22 @@ def wrapper(request):
for param in self.parameters.get('query', []):
error = self.validate_query_parameter(param, request)
if error:
response = problem(400, 'Bad Request', error)
return self.api.get_response(response)
raise BadRequestProblem(detail=error)

for param in self.parameters.get('path', []):
error = self.validate_path_parameter(param, request)
if error:
response = problem(400, 'Bad Request', error)
return self.api.get_response(response)
raise BadRequestProblem(detail=error)

for param in self.parameters.get('header', []):
error = self.validate_header_parameter(param, request)
if error:
response = problem(400, 'Bad Request', error)
return self.api.get_response(response)
raise BadRequestProblem(detail=error)

for param in self.parameters.get('formData', []):
error = self.validate_formdata_parameter(param["name"], param, request)
if error:
response = problem(400, 'Bad Request', error)
return self.api.get_response(response)
raise BadRequestProblem(detail=error)

return function(request)

Expand Down
35 changes: 26 additions & 9 deletions connexion/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from jsonschema.exceptions import ValidationError
from werkzeug.exceptions import Forbidden, Unauthorized

from .problem import problem


class ConnexionException(Exception):
pass
Expand All @@ -23,12 +21,6 @@ def __init__(self, status=400, title=None, detail=None, type=None,
self.headers = headers
self.ext = ext

def to_problem(self):
return problem(status=self.status, title=self.title, detail=self.detail,
type=self.type, instance=self.instance, headers=self.headers,
ext=self.ext)

badcure marked this conversation as resolved.
Show resolved Hide resolved

class ResolverError(LookupError):
def __init__(self, reason='Unknown reason', exc_info=None):
"""
Expand All @@ -52,12 +44,13 @@ class InvalidSpecification(ConnexionException, ValidationError):
pass


class NonConformingResponse(ConnexionException):
class NonConformingResponse(ProblemException):
def __init__(self, reason='Unknown Reason', message=None):
"""
:param reason: Reason why the response did not conform to the specification
:type reason: str
"""
super(NonConformingResponse, self).__init__(status=500, title=reason, detail=message)
self.reason = reason
self.message = message

Expand All @@ -68,6 +61,30 @@ def __repr__(self): # pragma: no cover
return '<NonConformingResponse: {}>'.format(self.reason)


class AuthenticationProblem(ProblemException):

def __init__(self, status, title, detail):
super(AuthenticationProblem, self).__init__(status=status, title=title, detail=detail)


class ResolverProblem(ProblemException):

def __init__(self, status, title, detail):
super(ResolverProblem, self).__init__(status=status, title=title, detail=detail)


class BadRequestProblem(ProblemException):

def __init__(self, title='Bad Request', detail=None):
super(BadRequestProblem, self).__init__(status=400, title=title, detail=detail)


class UnsupportedMediaTypeProblem(ProblemException):

def __init__(self, title="Unsupported Media Type", detail=None):
super(UnsupportedMediaTypeProblem, self).__init__(status=415, title=title, detail=detail)


class NonConformingResponseBody(NonConformingResponse):
def __init__(self, message, reason="Response body does not conform to specification"):
super(NonConformingResponseBody, self).__init__(reason=reason, message=message)
Expand Down
8 changes: 3 additions & 5 deletions connexion/handlers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging

from .operations.secure import SecureOperation
from .problem import problem
from .exceptions import AuthenticationProblem, ResolverProblem

logger = logging.getLogger('connexion.handlers')

Expand Down Expand Up @@ -45,12 +45,11 @@ def handle(self, *args, **kwargs):
"""
Actual handler for the execution after authentication.
"""
response = problem(
raise AuthenticationProblem(
badcure marked this conversation as resolved.
Show resolved Hide resolved
title=self.exception.name,
detail=self.exception.description,
status=self.exception.code
)
return self.api.get_response(response)


class ResolverErrorHandler(SecureOperation):
Expand All @@ -68,12 +67,11 @@ def function(self):
return self.handle

def handle(self, *args, **kwargs):
response = problem(
raise ResolverProblem(
title='Not Implemented',
detail=self.exception.reason,
status=self.status_code
)
return self.api.get_response(response)

@property
def operation_id(self):
Expand Down
26 changes: 13 additions & 13 deletions tests/fakeapi/hello.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import flask
from flask import jsonify, redirect

from connexion import NoContent, ProblemException, context, problem
from connexion import NoContent, ProblemException, context


class DummyClass(object):
Expand Down Expand Up @@ -92,19 +92,19 @@ def get_bye_secure_jwt(name, user, token_info):
return 'Goodbye {name} (Secure: {user})'.format(name=name, user=user)

def with_problem():
return problem(type='http://www.example.com/error',
title='Some Error',
detail='Something went wrong somewhere',
status=418,
instance='instance1',
headers={'x-Test-Header': 'In Test'})
raise ProblemException(type='http://www.example.com/error',
title='Some Error',
detail='Something went wrong somewhere',
status=418,
instance='instance1',
headers={'x-Test-Header': 'In Test'})


def with_problem_txt():
return problem(title='Some Error',
detail='Something went wrong somewhere',
status=418,
instance='instance1')
raise ProblemException(title='Some Error',
detail='Something went wrong somewhere',
status=418,
instance='instance1')


def internal_error():
Expand Down Expand Up @@ -404,8 +404,8 @@ def get_empty_dict():


def get_custom_problem_response():
return problem(403, "You need to pay", "Missing amount",
ext={'amount': 23.0})
raise ProblemException(403, "You need to pay", "Missing amount",
ext={'amount': 23.0})


def throw_problem_exception():
Expand Down
7 changes: 5 additions & 2 deletions tests/test_metrics.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import json

import flask
import pytest
from mock import MagicMock

import connexion
from connexion.exceptions import ProblemException
from connexion.decorators.metrics import UWSGIMetricsCollector


def test_timer(monkeypatch):
wrapper = UWSGIMetricsCollector('/foo/bar/<param>', 'get')

def operation(req):
return connexion.problem(418, '', '')
raise ProblemException(418, '', '')

op = wrapper(operation)
metrics = MagicMock()
monkeypatch.setattr('flask.request', MagicMock())
monkeypatch.setattr('flask.current_app', MagicMock(response_class=flask.Response))
monkeypatch.setattr('connexion.decorators.metrics.uwsgi_metrics', metrics)
op(MagicMock())
with pytest.raises(ProblemException) as exc:
op(MagicMock())
assert metrics.timer.call_args[0][:2] == ('connexion.response',
'418.GET.foo.bar.{param}')
Loading