Skip to content

Commit

Permalink
Merge pull request spec-first#186 from dfeinzeig/feature/issue-153-de…
Browse files Browse the repository at this point in the history
…tails-of-response-validation

Better error handling for non-conforming responses
  • Loading branch information
rafaelcaricio committed Mar 21, 2016
2 parents 83a439d + 191ec9a commit 593b533
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 17 deletions.
8 changes: 4 additions & 4 deletions connexion/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
import json
import logging
import pathlib
import yaml
import six
import sys
import werkzeug.exceptions
import yaml
from swagger_spec_validator.validator20 import validate_spec
from .operation import Operation
from . import utils
Expand Down Expand Up @@ -181,10 +183,8 @@ def add_paths(self, paths=None):
if self.debug:
logger.exception(error_msg)
else:
import sys
logger.error(error_msg)
et, ei, tb = sys.exc_info()
raise ei.with_traceback(tb)
six.reraise(*sys.exc_info())

def add_auth_on_not_found(self):
"""
Expand Down
27 changes: 17 additions & 10 deletions connexion/decorators/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
# Decorators to change the return type of endpoints
import functools
import logging
from ..exceptions import NonConformingResponse
from ..exceptions import NonConformingResponseBody, NonConformingResponseHeaders
from ..problem import problem
from .validation import RequestBodyValidator
from .validation import ResponseBodyValidator
from .decorator import BaseDecorator
from jsonschema import ValidationError


logger = logging.getLogger('connexion.decorators.response')
Expand Down Expand Up @@ -48,14 +49,18 @@ def validate_response(self, data, status_code, headers):

if response_definition and response_definition.get("schema"):
schema = response_definition.get("schema")
v = RequestBodyValidator(schema)
error = v.validate_schema(data)
if error:
raise NonConformingResponse("Response body does not conform to specification")
v = ResponseBodyValidator(schema)
try:
v.validate_schema(data)
except ValidationError as e:
raise NonConformingResponseBody(message=str(e))

if response_definition and response_definition.get("headers"):
if not all(item in headers.keys() for item in response_definition.get("headers").keys()):
raise NonConformingResponse("Response headers do not conform to specification")
response_definition_header_keys = response_definition.get("headers").keys()
if not all(item in headers.keys() for item in response_definition_header_keys):
raise NonConformingResponseHeaders(
message="Keys in header don't match response specification. Difference: %s"
% list(set(headers.keys()).symmetric_difference(set(response_definition_header_keys))))
return True

def __call__(self, function):
Expand All @@ -69,8 +74,10 @@ def wrapper(*args, **kwargs):
try:
data, status_code, headers = self.get_full_response(result)
self.validate_response(data, status_code, headers)
except NonConformingResponse as e:
return problem(500, 'Internal Server Error', e.reason)
except NonConformingResponseBody as e:
return problem(500, e.reason, e.message)
except NonConformingResponseHeaders as e:
return problem(500, e.reason, e.message)
return result

return wrapper
Expand Down
26 changes: 26 additions & 0 deletions connexion/decorators/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import functools
import itertools
import logging
import six
import sys
from jsonschema import draft4_format_checker, validate, ValidationError

from ..problem import problem
Expand Down Expand Up @@ -117,11 +119,35 @@ def validate_schema(self, data):
try:
validate(data, self.schema, format_checker=draft4_format_checker)
except ValidationError as exception:
logger.error("%s validation error: %s" % (flask.request.url, exception))
return problem(400, 'Bad Request', str(exception))

return None


class ResponseBodyValidator(object):
def __init__(self, schema, has_default=False):
"""
:param schema: The schema of the response body
:param has_default: Flag to indicate if default value is present.
"""
self.schema = schema
self.has_default = schema.get('default', has_default)

def validate_schema(self, data):
"""
:type schema: dict
:rtype: flask.Response | None
"""
try:
validate(data, self.schema, format_checker=draft4_format_checker)
except ValidationError as exception:
logger.error("%s validation error: %s" % (flask.request.url, exception))
six.reraise(*sys.exc_info())

return None


class ParameterValidator(object):
def __init__(self, parameters):
self.parameters = {k: list(g) for k, g in itertools.groupby(parameters, key=lambda p: p['in'])}
Expand Down
13 changes: 12 additions & 1 deletion connexion/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,26 @@ def __repr__(self): # pragma: no cover


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

def __str__(self): # pragma: no cover
return '<NonConformingResponse: {}>'.format(self.reason)

def __repr__(self): # pragma: no cover
return '<NonConformingResponse: {}>'.format(self.reason)


class NonConformingResponseBody(NonConformingResponse):
def __init__(self, message, reason="Response body does not conform to specification"):
super(NonConformingResponseBody, self).__init__(reason=reason, message=message)


class NonConformingResponseHeaders(NonConformingResponse):
def __init__(self, message, reason="Response headers do not conform to specification"):
super(NonConformingResponseHeaders, self).__init__(reason=reason, message=message)
4 changes: 2 additions & 2 deletions tests/api/test_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ def test_header_not_returned(simple_app):
assert response.content_type == 'application/problem+json'
data = json.loads(response.data.decode('utf-8'))
assert data['type'] == 'about:blank'
assert data['title'] == 'Internal Server Error'
assert data['detail'] == 'Response headers do not conform to specification'
assert data['title'] == 'Response headers do not conform to specification'
assert data['detail'] == "Keys in header don't match response specification. Difference: ['Location']"
assert data['status'] == 500


Expand Down

0 comments on commit 593b533

Please sign in to comment.