mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-09 20:37:46 +00:00
@@ -11,25 +11,32 @@ from .problem import problem
|
|||||||
|
|
||||||
|
|
||||||
class ConnexionException(Exception):
|
class ConnexionException(Exception):
|
||||||
pass
|
"""Base class for any exception thrown by the Connexion framework."""
|
||||||
|
|
||||||
|
|
||||||
class ResolverError(LookupError, ConnexionException):
|
class ResolverError(LookupError, ConnexionException):
|
||||||
pass
|
"""Error raised at startup when the resolver cannot find a view function for an endpoint in
|
||||||
|
your specification, and no ``resolver_error`` is configured."""
|
||||||
|
|
||||||
|
|
||||||
class InvalidSpecification(ValidationError, ConnexionException):
|
class InvalidSpecification(ValidationError, ConnexionException):
|
||||||
pass
|
"""Error raised at startup when the provided specification cannot be validated."""
|
||||||
|
|
||||||
|
|
||||||
class MissingMiddleware(ConnexionException):
|
class MissingMiddleware(ConnexionException):
|
||||||
pass
|
"""Error raised when you're leveraging behavior that depends on a specific middleware,
|
||||||
|
and that middleware is not part of your middleware stack."""
|
||||||
|
|
||||||
|
|
||||||
# HTTP ERRORS
|
# HTTP ERRORS
|
||||||
|
|
||||||
|
|
||||||
class ProblemException(HTTPException, ConnexionException):
|
class ProblemException(HTTPException, ConnexionException):
|
||||||
|
"""
|
||||||
|
This exception holds arguments that are going to be passed to the
|
||||||
|
`connexion.problem` function to generate a proper response.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -41,10 +48,6 @@ class ProblemException(HTTPException, ConnexionException):
|
|||||||
headers=None,
|
headers=None,
|
||||||
ext=None,
|
ext=None,
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
This exception holds arguments that are going to be passed to the
|
|
||||||
`connexion.problem` function to generate a proper response.
|
|
||||||
"""
|
|
||||||
self.status = self.status_code = status
|
self.status = self.status_code = status
|
||||||
self.title = title
|
self.title = title
|
||||||
self.detail = detail
|
self.detail = detail
|
||||||
@@ -68,30 +71,41 @@ class ProblemException(HTTPException, ConnexionException):
|
|||||||
# CLIENT ERRORS (4XX)
|
# CLIENT ERRORS (4XX)
|
||||||
|
|
||||||
|
|
||||||
class ClientError(ProblemException):
|
class ClientProblem(ProblemException):
|
||||||
|
"""Base exception for any 4XX error. Returns 400 by default, however
|
||||||
|
:class:`BadRequestProblem` should be preferred for 400 errors."""
|
||||||
|
|
||||||
def __init__(self, status: int = 400, title: str = None, *, detail: str = None):
|
def __init__(self, status: int = 400, title: str = None, *, detail: str = None):
|
||||||
super().__init__(status=status, title=title, detail=detail)
|
super().__init__(status=status, title=title, detail=detail)
|
||||||
|
|
||||||
|
|
||||||
class BadRequestProblem(ClientError):
|
class BadRequestProblem(ClientProblem):
|
||||||
|
"""Problem class for 400 Bad Request errors."""
|
||||||
|
|
||||||
def __init__(self, detail=None):
|
def __init__(self, detail=None):
|
||||||
super().__init__(status=400, title="Bad Request", detail=detail)
|
super().__init__(status=400, title="Bad Request", detail=detail)
|
||||||
|
|
||||||
|
|
||||||
class ExtraParameterProblem(BadRequestProblem):
|
class ExtraParameterProblem(BadRequestProblem):
|
||||||
|
"""Problem class for 400 Bad Request errors raised when extra query or form parameters are
|
||||||
|
detected and ``strict_validation`` is enabled."""
|
||||||
|
|
||||||
def __init__(self, *, param_type: str, extra_params: t.Iterable[str]):
|
def __init__(self, *, param_type: str, extra_params: t.Iterable[str]):
|
||||||
detail = f"Extra {param_type} parameter(s) {','.join(extra_params)} not in spec"
|
detail = f"Extra {param_type} parameter(s) {','.join(extra_params)} not in spec"
|
||||||
super().__init__(detail=detail)
|
super().__init__(detail=detail)
|
||||||
|
|
||||||
|
|
||||||
class TypeValidationError(BadRequestProblem):
|
class TypeValidationError(BadRequestProblem):
|
||||||
|
"""Problem class for 400 Bad Request errors raised when path, query or form parameters with
|
||||||
|
an incorrect type are detected."""
|
||||||
|
|
||||||
def __init__(self, schema_type: str, parameter_type: str, parameter_name: str):
|
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}'"
|
detail = f"Wrong type, expected '{schema_type}' for {parameter_type} parameter '{parameter_name}'"
|
||||||
super().__init__(detail=detail)
|
super().__init__(detail=detail)
|
||||||
|
|
||||||
|
|
||||||
class Unauthorized(ClientError):
|
class Unauthorized(ClientProblem):
|
||||||
|
"""Problem class for 401 Unauthorized errors."""
|
||||||
|
|
||||||
description = (
|
description = (
|
||||||
"The server could not verify that you are authorized to access"
|
"The server could not verify that you are authorized to access"
|
||||||
@@ -105,14 +119,22 @@ class Unauthorized(ClientError):
|
|||||||
|
|
||||||
|
|
||||||
class OAuthProblem(Unauthorized):
|
class OAuthProblem(Unauthorized):
|
||||||
|
"""Problem class for 401 Unauthorized errors raised when there is an issue with the received
|
||||||
|
OAuth headers."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class OAuthResponseProblem(OAuthProblem):
|
class OAuthResponseProblem(OAuthProblem):
|
||||||
|
"""Problem class for 401 Unauthorized errors raised when improper OAuth credentials are
|
||||||
|
retrieved from your OAuth server."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Forbidden(HTTPException):
|
class Forbidden(HTTPException):
|
||||||
|
"""Problem class for 403 Unauthorized errors."""
|
||||||
|
|
||||||
def __init__(self, detail: t.Optional[str] = None):
|
def __init__(self, detail: t.Optional[str] = None):
|
||||||
if detail is None:
|
if detail is None:
|
||||||
detail = (
|
detail = (
|
||||||
@@ -124,6 +146,8 @@ class Forbidden(HTTPException):
|
|||||||
|
|
||||||
|
|
||||||
class OAuthScopeProblem(Forbidden):
|
class OAuthScopeProblem(Forbidden):
|
||||||
|
"""Problem class for 403 Unauthorized errors raised because of OAuth scope validation errors."""
|
||||||
|
|
||||||
def __init__(self, token_scopes: list, required_scopes: list) -> None:
|
def __init__(self, token_scopes: list, required_scopes: list) -> None:
|
||||||
self.required_scopes = required_scopes
|
self.required_scopes = required_scopes
|
||||||
self.token_scopes = token_scopes
|
self.token_scopes = token_scopes
|
||||||
@@ -134,7 +158,10 @@ class OAuthScopeProblem(Forbidden):
|
|||||||
super().__init__(detail=detail)
|
super().__init__(detail=detail)
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedMediaTypeProblem(ClientError):
|
class UnsupportedMediaTypeProblem(ClientProblem):
|
||||||
|
"""Problem class for 415 Unsupported Media Type errors which are raised when Connexion
|
||||||
|
receives a request with an unsupported media type header."""
|
||||||
|
|
||||||
def __init__(self, detail: t.Optional[str] = None):
|
def __init__(self, detail: t.Optional[str] = None):
|
||||||
super().__init__(status=415, title="Unsupported Media Type", detail=detail)
|
super().__init__(status=415, title="Unsupported Media Type", detail=detail)
|
||||||
|
|
||||||
@@ -143,6 +170,9 @@ class UnsupportedMediaTypeProblem(ClientError):
|
|||||||
|
|
||||||
|
|
||||||
class ServerError(ProblemException):
|
class ServerError(ProblemException):
|
||||||
|
"""Base exception for any 5XX error. Returns 500 by default, however
|
||||||
|
:class:`InternalServerError` should be preferred for 500 errors."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
status: int = 500,
|
status: int = 500,
|
||||||
@@ -157,6 +187,8 @@ class ServerError(ProblemException):
|
|||||||
|
|
||||||
|
|
||||||
class InternalServerError(ServerError):
|
class InternalServerError(ServerError):
|
||||||
|
"""Problem class for 500 Internal Server errors."""
|
||||||
|
|
||||||
def __init__(self, detail: t.Optional[str] = None):
|
def __init__(self, detail: t.Optional[str] = None):
|
||||||
if detail is None:
|
if detail is None:
|
||||||
detail = (
|
detail = (
|
||||||
@@ -167,11 +199,17 @@ class InternalServerError(ServerError):
|
|||||||
|
|
||||||
|
|
||||||
class NonConformingResponse(InternalServerError):
|
class NonConformingResponse(InternalServerError):
|
||||||
|
"""Problem class for 500 Internal Server errors raised because of a returned response not
|
||||||
|
matching the specification if response validation is enabled."""
|
||||||
|
|
||||||
def __init__(self, detail: t.Optional[str] = None):
|
def __init__(self, detail: t.Optional[str] = None):
|
||||||
super().__init__(detail=detail)
|
super().__init__(detail=detail)
|
||||||
|
|
||||||
|
|
||||||
class NonConformingResponseBody(NonConformingResponse):
|
class NonConformingResponseBody(NonConformingResponse):
|
||||||
|
"""Problem class for 500 Internal Server errors raised because of a returned response body not
|
||||||
|
matching the specification if response validation is enabled."""
|
||||||
|
|
||||||
def __init__(self, detail: t.Optional[str] = None):
|
def __init__(self, detail: t.Optional[str] = None):
|
||||||
if detail is None:
|
if detail is None:
|
||||||
detail = "Response body does not conform to specification"
|
detail = "Response body does not conform to specification"
|
||||||
@@ -180,6 +218,9 @@ class NonConformingResponseBody(NonConformingResponse):
|
|||||||
|
|
||||||
|
|
||||||
class NonConformingResponseHeaders(NonConformingResponse):
|
class NonConformingResponseHeaders(NonConformingResponse):
|
||||||
|
"""Problem class for 500 Internal Server errors raised because of a returned response headers
|
||||||
|
not matching the specification if response validation is enabled."""
|
||||||
|
|
||||||
def __init__(self, detail: t.Optional[str] = None):
|
def __init__(self, detail: t.Optional[str] = None):
|
||||||
if detail is None:
|
if detail is None:
|
||||||
detail = "Response headers do not conform to specification"
|
detail = "Response headers do not conform to specification"
|
||||||
@@ -188,5 +229,8 @@ class NonConformingResponseHeaders(NonConformingResponse):
|
|||||||
|
|
||||||
|
|
||||||
class ResolverProblem(ServerError):
|
class ResolverProblem(ServerError):
|
||||||
|
"""Problem class for 501 Not Implemented errors raised when the resolver cannot find a view
|
||||||
|
function to handle the incoming request."""
|
||||||
|
|
||||||
def __init__(self, status: int = 501, *, detail: t.Optional[str] = None):
|
def __init__(self, status: int = 501, *, detail: t.Optional[str] = None):
|
||||||
super().__init__(status=status, title="Not Implemented", detail=detail)
|
super().__init__(status=status, title="Not Implemented", detail=detail)
|
||||||
|
|||||||
@@ -1,146 +1,149 @@
|
|||||||
Exception Handling
|
Exception Handling
|
||||||
==================
|
==================
|
||||||
Rendering Exceptions through the Flask Handler
|
|
||||||
----------------------------------------------
|
|
||||||
Flask by default contains an exception handler, which connexion's app can proxy
|
|
||||||
to with the ``add_error_handler`` method. You can hook either on status codes
|
|
||||||
or on a specific exception type.
|
|
||||||
|
|
||||||
Connexion is moving from returning flask responses on errors to throwing exceptions
|
Connexion allows you to register custom error handlers to convert Python ``Exceptions`` into HTTP
|
||||||
that are a subclass of ``connexion.problem``. So far exceptions thrown in the OAuth
|
problem responses.
|
||||||
decorator have been converted.
|
|
||||||
|
|
||||||
Flask Error Handler Example
|
.. tab-set::
|
||||||
---------------------------
|
|
||||||
|
|
||||||
The goal here is to make the api returning the 404 status code
|
.. tab-item:: AsyncApp
|
||||||
when there is a NotFoundException (instead of 500)
|
:sync: AsyncApp
|
||||||
|
|
||||||
.. code-block:: python
|
You can register error handlers on:
|
||||||
|
|
||||||
def test_should_return_404(client):
|
- The exception class to handle
|
||||||
invalid_id = 0
|
If this exception class is raised somewhere in your application or the middleware stack,
|
||||||
response = client.get(f"/api/data/{invalid_id}")
|
it will be passed to your handler.
|
||||||
assert response.status_code == 404
|
- The HTTP status code to handle
|
||||||
|
Connexion will raise ``starlette.HTTPException`` errors when it encounters any issues
|
||||||
|
with a request or response. You can intercept these exceptions with specific status codes
|
||||||
|
if you want to return custom responses.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
Firstly, it's possible to declare what Exception must be handled
|
from connexion import AsyncApp
|
||||||
|
from connexion.lifecycle import ConnexionRequest, ConnexionResponse
|
||||||
|
|
||||||
.. code-block:: python
|
def not_found(request: ConnexionRequest, exc: Exception) -> ConnexionResponse:
|
||||||
|
return ConnexionResponse(status_code=404, body=json.dumps({"error": "NotFound"}))
|
||||||
|
|
||||||
# exceptions.py
|
app = AsyncApp(__name__)
|
||||||
class NotFoundException(RuntimeError):
|
app.add_error_handler(FileNotFoundError, not_found)
|
||||||
"""Not found."""
|
app.add_error_handler(404, not_found)
|
||||||
|
|
||||||
class MyDataNotFound(NotFoundException):
|
.. dropdown:: View a detailed reference of the :code:`add_middleware` method
|
||||||
def __init__(self, id):
|
:icon: eye
|
||||||
super().__init__(f"ID '{id}' not found.")
|
|
||||||
|
|
||||||
|
.. automethod:: connexion.AsyncApp.add_error_handler
|
||||||
|
:noindex:
|
||||||
|
|
||||||
# init flask app
|
.. tab-item:: FlaskApp
|
||||||
import connexion
|
:sync: FlaskApp
|
||||||
|
|
||||||
def not_found_handler(error):
|
You can register error handlers on:
|
||||||
return {
|
|
||||||
"detail": str(error),
|
|
||||||
"status": 404,
|
|
||||||
"title": "Not Found",
|
|
||||||
}, 404
|
|
||||||
|
|
||||||
def create_app():
|
- The exception class to handle
|
||||||
|
If this exception class is raised somewhere in your application or the middleware stack,
|
||||||
|
it will be passed to your handler.
|
||||||
|
- The HTTP status code to handle
|
||||||
|
Connexion will raise ``starlette.HTTPException`` errors when it encounters any issues
|
||||||
|
with a request or response. The underlying Flask application will raise
|
||||||
|
``werkzeug.HTTPException`` errors. You can intercept both of these exceptions with
|
||||||
|
specific status codes if you want to return custom responses.
|
||||||
|
|
||||||
connexion_app = connexion.FlaskApp(
|
.. code-block:: python
|
||||||
__name__, specification_dir="../api/")
|
|
||||||
connexion_app.add_api(
|
|
||||||
"openapi.yaml", validate_responses=True,
|
|
||||||
base_path="/")
|
|
||||||
|
|
||||||
# Handle NotFoundException
|
from connexion import FlaskApp
|
||||||
connexion_app.add_error_handler(
|
from connexion.lifecycle import ConnexionRequest, ConnexionResponse
|
||||||
NotFoundException, not_found_handler)
|
|
||||||
|
|
||||||
app = connexion_app.app
|
def not_found(request: ConnexionRequest, exc: Exception) -> ConnexionResponse:
|
||||||
return app
|
return ConnexionResponse(status_code=404, body=json.dumps({"error": "NotFound"}))
|
||||||
|
|
||||||
In this way, it's possible to raise anywhere the NotFoundException or its subclasses
|
app = FlaskApp(__name__)
|
||||||
and we know the API will return 404 status code.
|
app.add_error_handler(FileNotFoundError, not_found)
|
||||||
|
app.add_error_handler(404, not_found)
|
||||||
|
|
||||||
.. code-block:: python
|
.. dropdown:: View a detailed reference of the :code:`add_middleware` method
|
||||||
|
:icon: eye
|
||||||
|
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
.. automethod:: connexion.FlaskApp.add_error_handler
|
||||||
|
:noindex:
|
||||||
|
|
||||||
from .exceptions import MyDataNotFound
|
.. tab-item:: ConnexionMiddleware
|
||||||
from .models import MyData
|
:sync: ConnexionMiddleware
|
||||||
|
|
||||||
|
You can register error handlers on:
|
||||||
|
|
||||||
def get_my_data(id, token_info=None):
|
- The exception class to handle
|
||||||
try:
|
If this exception class is raised somewhere in your application or the middleware stack,
|
||||||
data = MyData.query.filter(MyData.id == id).one()
|
it will be passed to your handler.
|
||||||
|
- The HTTP status code to handle
|
||||||
|
Connexion will raise ``starlette.HTTPException`` errors when it encounters any issues
|
||||||
|
with a request or response. You can intercept these exceptions with specific status codes
|
||||||
|
if you want to return custom responses.
|
||||||
|
Note that this might not catch ``HTTPExceptions`` with the same status code raised by
|
||||||
|
your wrapped ASGI/WSGI framework.
|
||||||
|
|
||||||
return {
|
.. code-block:: python
|
||||||
"id": data.id,
|
|
||||||
"description": data.description,
|
|
||||||
}
|
|
||||||
|
|
||||||
except NoResultFound:
|
from asgi_framework import App
|
||||||
raise MyDataNotFound(id)
|
from connexion import ConnexionMiddleware
|
||||||
|
from connexion.lifecycle import ConnexionRequest, ConnexionResponse
|
||||||
|
|
||||||
|
def not_found(request: ConnexionRequest, exc: Exception) -> ConnexionResponse:
|
||||||
|
return ConnexionResponse(status_code=404, body=json.dumps({"error": "NotFound"}))
|
||||||
|
|
||||||
|
app = App(__name__)
|
||||||
|
app = ConnexionMiddleware(app)
|
||||||
|
|
||||||
|
app.add_error_handler(FileNotFoundError, not_found)
|
||||||
|
app.add_error_handler(404, not_found)
|
||||||
|
|
||||||
|
.. dropdown:: View a detailed reference of the :code:`add_middleware` method
|
||||||
|
:icon: eye
|
||||||
|
|
||||||
|
.. automethod:: connexion.ConnexionMiddleware.add_error_handler
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Error handlers can be ``async`` coroutines as well.
|
||||||
|
|
||||||
Default Exception Handling
|
Default Exception Handling
|
||||||
--------------------------
|
--------------------------
|
||||||
By default connexion exceptions are JSON serialized according to
|
By default connexion exceptions are JSON serialized according to
|
||||||
`Problem Details for HTTP APIs`_
|
`Problem Details for HTTP APIs`_
|
||||||
|
|
||||||
Application can return errors using ``connexion.problem`` or exceptions that inherit from both
|
Application can return errors using ``connexion.problem.problem`` or raise exceptions that inherit
|
||||||
``connexion.ProblemException`` and a ``werkzeug.exceptions.HttpException`` subclass (for example
|
either from ``connexion.ProblemException`` or one of its subclasses to achieve the same behavior.
|
||||||
``werkzeug.exceptions.Forbidden``). An example of this is the ``connexion.exceptions.OAuthProblem``
|
|
||||||
exception
|
Using this, we can rewrite the handler above:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class OAuthProblem(ProblemException, Unauthorized):
|
from connexion.lifecycle import ConnexionRequest, ConnexionResponse
|
||||||
def __init__(self, title=None, **kwargs):
|
from connexion.problem import problem
|
||||||
super(OAuthProblem, self).__init__(title=title, **kwargs)
|
|
||||||
|
def not_found(request: ConnexionRequest, exc: Exception) -> ConnexionResponse:
|
||||||
|
return problem(
|
||||||
|
title="NotFound",
|
||||||
|
detail="The requested resource was not found on the server",
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
.. dropdown:: View a detailed reference of the :code:`problem` function
|
||||||
|
:icon: eye
|
||||||
|
|
||||||
|
.. autofunction:: connexion.problem.problem
|
||||||
|
|
||||||
.. _Problem Details for HTTP APIs: https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
|
.. _Problem Details for HTTP APIs: https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
|
||||||
|
|
||||||
Examples of Custom Rendering Exceptions
|
Connexion Exceptions
|
||||||
---------------------------------------
|
--------------------
|
||||||
To custom render an exception when you boot your connexion application you can hook into a custom
|
|
||||||
exception and render it in some sort of custom format. For example
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from flask import Response
|
|
||||||
import connexion
|
|
||||||
from connexion.exceptions import OAuthResponseProblem
|
|
||||||
|
|
||||||
def render_unauthorized(exception):
|
|
||||||
return Response(response=json.dumps({'error': 'There is an error in the oAuth token supplied'}), status=401, mimetype="application/json")
|
|
||||||
|
|
||||||
app = connexion.FlaskApp(__name__, specification_dir='./../swagger/', debug=False, swagger_ui=False)
|
|
||||||
app.add_error_handler(OAuthResponseProblem, render_unauthorized)
|
|
||||||
|
|
||||||
Custom Exceptions
|
|
||||||
-----------------
|
|
||||||
There are several exception types in connexion that contain extra information to help you render appropriate
|
There are several exception types in connexion that contain extra information to help you render appropriate
|
||||||
messages to your user beyond the default description and status code:
|
messages to your user beyond the default description and status code:
|
||||||
|
|
||||||
OAuthProblem
|
.. automodule:: connexion.exceptions
|
||||||
^^^^^^^^^^^^
|
:members:
|
||||||
This exception is thrown when there is some sort of validation issue with the Authorisation Header
|
:show-inheritance:
|
||||||
|
:member-order: bysource
|
||||||
OAuthResponseProblem
|
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
|
||||||
This exception is thrown when there is a validation issue from your OAuth 2 Server. It contains a
|
|
||||||
``token_response`` property which contains the full http response from the OAuth 2 Server
|
|
||||||
|
|
||||||
OAuthScopeProblem
|
|
||||||
^^^^^^^^^^^^^^^^^
|
|
||||||
This scope indicates the OAuth 2 Server did not generate a token with all the scopes required. This
|
|
||||||
contains 3 properties
|
|
||||||
- ``required_scopes`` - The scopes that were required for this endpoint
|
|
||||||
- ``token_scopes`` - The scopes that were granted for this endpoint
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user