Skip to content

Commit

Permalink
Clean up errors and fix hierarchy (#1649)
Browse files Browse the repository at this point in the history
Fixes #1414 
Fixes #590
Fixes #945 
Fixes #958 
Fixes #964 

And generally cleans up the exception module with a consistent interface
across errors and a correct hierarchy.
  • Loading branch information
RobbeSneyders committed Feb 22, 2023
2 parents 2f9a461 + ec5dd27 commit 90a734d
Show file tree
Hide file tree
Showing 22 changed files with 180 additions and 231 deletions.
2 changes: 0 additions & 2 deletions connexion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
specified.
"""

import werkzeug.exceptions as exceptions # NOQA

from .apps import AbstractApp # NOQA
from .apps.asynchronous import AsyncApp
from .datastructures import NoContent # NOQA
Expand Down
14 changes: 3 additions & 11 deletions connexion/apps/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from connexion.apps.abstract import AbstractApp
from connexion.decorators import FlaskDecorator
from connexion.exceptions import ProblemException, ResolverError
from connexion.exceptions import InternalServerError, ProblemException, ResolverError
from connexion.frameworks import flask as flask_utils
from connexion.jsonifier import Jsonifier
from connexion.middleware.abstract import AbstractRoutingAPI, SpecMiddleware
Expand Down Expand Up @@ -127,18 +127,10 @@ def set_errors_handlers(self):
def common_error_handler(self, exception: Exception) -> FlaskResponse:
"""Default error handler."""
if isinstance(exception, ProblemException):
response = problem(
status=exception.status,
title=exception.title,
detail=exception.detail,
type=exception.type,
instance=exception.instance,
headers=exception.headers,
ext=exception.ext,
)
response = exception.to_problem()
else:
if not isinstance(exception, werkzeug.exceptions.HTTPException):
exception = werkzeug.exceptions.InternalServerError()
exception = InternalServerError()

response = problem(
title=exception.name,
Expand Down
233 changes: 99 additions & 134 deletions connexion/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
This module defines Exception classes used by Connexion to generate a proper response.
"""

import warnings
import typing as t

from jsonschema.exceptions import ValidationError
from starlette.exceptions import HTTPException
Expand All @@ -14,10 +14,26 @@ class ConnexionException(Exception):
pass


class ProblemException(ConnexionException):
class ResolverError(LookupError, ConnexionException):
pass


class InvalidSpecification(ValidationError, ConnexionException):
pass


class MissingMiddleware(ConnexionException):
pass


# HTTP ERRORS


class ProblemException(HTTPException, ConnexionException):
def __init__(
self,
status=400,
*,
status=500,
title=None,
detail=None,
type=None,
Expand All @@ -29,7 +45,7 @@ def __init__(
This exception holds arguments that are going to be passed to the
`connexion.problem` function to generate a proper response.
"""
self.status = status
self.status = self.status_code = status
self.title = title
self.detail = detail
self.type = type
Expand All @@ -38,11 +54,6 @@ def __init__(
self.ext = ext

def to_problem(self):
warnings.warn(
"'to_problem' is planned to be removed in a future release. "
"Call connexion.problem.problem(..) instead to maintain the existing error response.",
DeprecationWarning,
)
return problem(
status=self.status,
title=self.title,
Expand All @@ -54,96 +65,33 @@ def to_problem(self):
)


class ResolverError(LookupError):
def __init__(self, reason="Unknown reason", exc_info=None):
"""
:param reason: Reason why the resolver failed.
:type reason: str
:param exc_info: If specified, gives details of the original exception
as returned by sys.exc_info()
:type exc_info: tuple | None
"""
self.reason = reason
self.exc_info = exc_info

def __str__(self): # pragma: no cover
return f"<ResolverError: {self.reason}>"

def __repr__(self): # pragma: no cover
return f"<ResolverError: {self.reason}>"

# CLIENT ERRORS (4XX)

class InvalidSpecification(ConnexionException, ValidationError):
pass


class MissingMiddleware(ConnexionException):
pass


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().__init__(status=500, title=reason, detail=message)
self.reason = reason
self.message = message

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

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


class AuthenticationProblem(ProblemException):
def __init__(self, status, title, detail):
class ClientError(ProblemException):
def __init__(self, status: int = 400, title: str = None, *, detail: str = None):
super().__init__(status=status, title=title, detail=detail)


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


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


class NotFoundProblem(ProblemException):

description = (
"The requested URL was not found on the server. If you entered the URL manually please "
"check your spelling and try again."
)

def __init__(self, title="Not Found", detail=description):
super().__init__(status=404, title=title, detail=detail)
class BadRequestProblem(ClientError):
def __init__(self, detail=None):
super().__init__(status=400, title="Bad Request", detail=detail)


class UnsupportedMediaTypeProblem(ProblemException):
def __init__(self, title="Unsupported Media Type", detail=None):
super().__init__(status=415, title=title, detail=detail)
class ExtraParameterProblem(BadRequestProblem):
def __init__(self, *, param_type: str, extra_params: t.Iterable[str]):
detail = f"Extra {param_type} parameter(s) {','.join(extra_params)} not in spec"
super().__init__(detail=detail)


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


class NonConformingResponseHeaders(NonConformingResponse):
def __init__(
self, message, reason="Response headers do not conform to specification"
):
super().__init__(reason=reason, message=message)
class TypeValidationError(BadRequestProblem):
def __init__(self, schema_type: str, parameter_type: str, parameter_name: str):
"""Exception raised when type validation fails"""
detail = f"Wrong type, expected '{schema_type}' for {parameter_type} parameter '{parameter_name}'"
super().__init__(detail=detail)


class Unauthorized(HTTPException):
class Unauthorized(ClientError):

description = (
"The server could not verify that you are authorized to access"
Expand All @@ -152,76 +100,93 @@ class Unauthorized(HTTPException):
" how to supply the credentials required."
)

def __init__(self, detail: str = description, **kwargs):
super().__init__(401, detail=detail, **kwargs)
def __init__(self, detail: str = description):
super().__init__(401, title="Unauthorized", detail=detail)


class OAuthProblem(Unauthorized):
pass


class OAuthResponseProblem(OAuthProblem):
def __init__(self, token_response, **kwargs):
self.token_response = token_response
super().__init__(**kwargs)
pass


class Forbidden(HTTPException):

description = (
"You don't have the permission to access the requested"
" resource. It is either read-protected or not readable by the"
" server."
)

def __init__(self, detail: str = description, **kwargs):
super().__init__(403, detail=detail, **kwargs)
def __init__(self, detail: t.Optional[str] = None):
if detail is None:
detail = (
"You don't have the permission to access the requested"
" resource. It is either read-protected or not readable by the"
" server."
)
super().__init__(403, detail=detail)


class OAuthScopeProblem(Forbidden):
def __init__(self, token_scopes, required_scopes, **kwargs):
def __init__(self, token_scopes: list, required_scopes: list) -> None:
self.required_scopes = required_scopes
self.token_scopes = token_scopes
detail = (
f"Provided token does not have the required scopes. "
f"Provided: {token_scopes}; Required: {required_scopes}"
)
super().__init__(detail=detail)

super().__init__(**kwargs)

class UnsupportedMediaTypeProblem(ClientError):
def __init__(self, detail: t.Optional[str] = None):
super().__init__(status=415, title="Unsupported Media Type", detail=detail)

class ExtraParameterProblem(ProblemException):

# SERVER ERRORS (5XX)


class ServerError(ProblemException):
def __init__(
self, formdata_parameters, query_parameters, title=None, detail=None, **kwargs
self,
status: int = 500,
title: t.Optional[str] = None,
*,
detail: t.Optional[str] = None,
):
self.extra_formdata = formdata_parameters
self.extra_query = query_parameters
if title is None:
title = "Internal Server Error"

# This keep backwards compatibility with the old returns
super().__init__(status=status, title=title, detail=detail)


class InternalServerError(ServerError):
def __init__(self, detail: t.Optional[str] = None):
if detail is None:
if self.extra_query:
detail = "Extra {parameter_type} parameter(s) {extra_params} not in spec".format(
parameter_type="query", extra_params=", ".join(self.extra_query)
)
elif self.extra_formdata:
detail = "Extra {parameter_type} parameter(s) {extra_params} not in spec".format(
parameter_type="formData",
extra_params=", ".join(self.extra_formdata),
)
detail = (
"The server encountered an internal error and was unable to complete your "
"request. Either the server is overloaded or there is an error in the application."
)
super().__init__(status=500, title="Internal Server Error", detail=detail)

super().__init__(title=title, detail=detail, **kwargs)

class NonConformingResponse(InternalServerError):
def __init__(self, detail: t.Optional[str] = None):
super().__init__(detail=detail)

class TypeValidationError(Exception):
def __init__(self, schema_type, parameter_type, parameter_name):
"""
Exception raise when type validation fails

:type schema_type: str
:type parameter_type: str
:type parameter_name: str
:return:
"""
self.schema_type = schema_type
self.parameter_type = parameter_type
self.parameter_name = parameter_name
class NonConformingResponseBody(NonConformingResponse):
def __init__(self, detail: t.Optional[str] = None):
if detail is None:
detail = "Response body does not conform to specification"

super().__init__(detail=detail)


class NonConformingResponseHeaders(NonConformingResponse):
def __init__(self, detail: t.Optional[str] = None):
if detail is None:
detail = "Response headers do not conform to specification"

super().__init__(detail=detail)


def __str__(self):
msg = "Wrong type, expected '{schema_type}' for {parameter_type} parameter '{parameter_name}'"
return msg.format(**vars(self))
class ResolverProblem(ServerError):
def __init__(self, status: int = 501, *, detail: t.Optional[str] = None):
super().__init__(status=status, title="Not Implemented", detail=detail)
6 changes: 2 additions & 4 deletions connexion/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ def function(self):

def handle(self, *args, **kwargs):
raise ResolverProblem(
title="Not Implemented",
detail=self.exception.reason,
detail=self.exception.args[0],
status=self.status_code,
)

Expand Down Expand Up @@ -55,7 +54,6 @@ def get_mimetype(self):

async def __call__(self, *args, **kwargs):
raise ResolverProblem(
title="Not Implemented",
detail=self.exception.reason,
detail=self.exception.args[0],
status=self.status_code,
)

0 comments on commit 90a734d

Please sign in to comment.