mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-06 04:19:26 +00:00
I was writing the documentation on exception handling, and I noticed that it was very hard to explain our current behavior. Error handlers can be registered either on the internal Flask app (not the Starlette one) or on the Connexion app, which leads to some undefined (actually just really hard to explain) behavior. Eg. - Registering error handlers on a status code would capture `starlette.HTTPException` errors on the Connexion app, and `werkzeug.HTTPException` errors on the Flask App, which means that registering an error handler on a status code doesn't catch all the errors with that status code. - Flask does some default error handling which leads to some exceptions never reaching the error handlers registered on the Connexion app. So I made the following changes: - Replaced the default error handlers we registered on the Flask app with a default handler on the `ExceptionMiddleware` that takes into account other handlers registered on status codes. - Configured Flask to propagate exceptions instead of catching them. - Abstracted away the Starlette `Request` and `Response` types, so users can and must now use `ConnexionRequest` and `ConnexionResponse` types in error handlers. - Renamed the `ASGIRequest` class to `ConnexionRequest` since it is the only Request class part of the high level Connexion interface. We could also rename `ConnexionRequest` and `ConnexionResponse` to just `Request` and `Response`. Wdyt?
499 lines
15 KiB
ReStructuredText
499 lines
15 KiB
ReStructuredText
Request handling
|
|
================
|
|
|
|
When your application receives a request, Connexion provides a lot of functionality based on your
|
|
OpenAPI spec:
|
|
|
|
- It checks the security (see :doc:`security`)
|
|
- It routes the request to the correct endpoint (see :doc:`routing`)
|
|
- It validates the body and parameters (see :doc:`validation`)
|
|
- It parses and passes the body and parameters to your python function
|
|
|
|
On this page, we zoom in on the final part.
|
|
|
|
Automatic parameter handling
|
|
----------------------------
|
|
|
|
.. tab-set::
|
|
|
|
.. tab-item:: AsyncApp
|
|
:sync: AsyncApp
|
|
|
|
Connexion automatically maps the parameters defined in your endpoint specification to the
|
|
arguments defined your associated Python function, parsing and casting values when
|
|
possible. All you need to do, is make sure the arguments of your function match the
|
|
parameters in your specification.
|
|
|
|
.. tab-item:: FlaskApp
|
|
:sync: FlaskApp
|
|
|
|
Connexion automatically maps the parameters defined in your endpoint specification to the
|
|
arguments defined your associated Python function, parsing and casting values when
|
|
possible. All you need to do, is make sure the arguments of your function match the
|
|
parameters in your specification.
|
|
|
|
.. tab-item:: ConnexionMiddleware
|
|
:sync: ConnexionMiddleware
|
|
|
|
Connexion can automatically map the parameters defined in your endpoint specification to
|
|
the arguments defined your associated Python function, parsing and casting values when
|
|
possible. All you need to do, is make sure the arguments of your function match the
|
|
parameters in your specification.
|
|
|
|
To activate this behavior when using the ``ConnexionMiddleware`` wrapping a third party
|
|
application, you can leverage the following decorators provided by Connexion:
|
|
|
|
* ``WSGIDecorator``: provides automatic parameter injection for WSGI applications. Note
|
|
that this decorator injects Werkzeug / Flask datastructures.
|
|
|
|
* ``FlaskDecorator``: provides automatic parameter injection and response serialization for
|
|
Flask applications.
|
|
|
|
* ``ASGIDecorator``: provides automatic parameter injection for ASGI applications. Note that
|
|
this decorator injects Starlette datastructures (such as UploadFile).
|
|
|
|
* ``StarletteDecorator``: provides automatic parameter injection and response serialization
|
|
for Starlette applications.
|
|
|
|
.. code-block:: python
|
|
:caption: **app.py**
|
|
|
|
from asgi_framework import App
|
|
from connexion import ConnexionMiddleware
|
|
from connexion.decorators import ASGIDecorator
|
|
|
|
@app.route("/greeting/<name>", methods=["POST"])
|
|
@ASGIDecorator()
|
|
def post_greeting(name):
|
|
...
|
|
|
|
app = App(__name__)
|
|
app = ConnexionMiddleware(app)
|
|
app.add_api("openapi.yaml")
|
|
|
|
For a full example, see our `Frameworks`_ example.
|
|
|
|
For example, if you have an endpoint specified as:
|
|
|
|
.. tab-set::
|
|
|
|
.. tab-item:: OpenAPI 3
|
|
:sync: OpenAPI 3
|
|
|
|
.. code-block:: yaml
|
|
:caption: **openapi.yaml**
|
|
|
|
paths:
|
|
/foo:
|
|
get:
|
|
operationId: api.foo_get
|
|
parameters:
|
|
- name: message
|
|
description: Some message.
|
|
in: query
|
|
schema:
|
|
type: string
|
|
required: true
|
|
|
|
.. tab-item:: Swagger 2
|
|
:sync: Swagger 2
|
|
|
|
.. code-block:: yaml
|
|
:caption: **swagger.yaml**
|
|
|
|
paths:
|
|
/foo:
|
|
get:
|
|
operationId: api.foo_get
|
|
parameters:
|
|
- name: message
|
|
description: Some message.
|
|
in: query
|
|
type: string
|
|
required: true
|
|
|
|
And the view function as:
|
|
|
|
.. code-block:: python
|
|
:caption: **api.py**
|
|
|
|
def foo_get(message):
|
|
...
|
|
|
|
Connexion will automatically identify that your view function expects an argument named ``message``
|
|
and will pass in the value of the endpoint parameter ``message``.
|
|
|
|
This works for both path and query parameters.
|
|
|
|
Body
|
|
----
|
|
|
|
The body will also be passed to your function.
|
|
|
|
.. tab-set::
|
|
|
|
.. tab-item:: OpenAPI 3
|
|
:sync: OpenAPI 3
|
|
|
|
In the OpenAPI 3 spec, the ``requestBody`` does not have a name. By default it will be
|
|
passed into your function as ``body``. You can use ``x-body-name`` in your operation to
|
|
override this name.
|
|
|
|
.. code-block:: yaml
|
|
:caption: **openapi.yaml**
|
|
|
|
paths:
|
|
/path
|
|
post:
|
|
operationId: api.foo_get
|
|
requestBody:
|
|
x-body-name: payload
|
|
content:
|
|
application/json:
|
|
schema:
|
|
...
|
|
|
|
.. code-block:: python
|
|
:caption: **api.py**
|
|
|
|
# Default
|
|
def foo_get(body)
|
|
...
|
|
|
|
# Based on x-body-name
|
|
def foo_get(payload)
|
|
...
|
|
|
|
.. tab-item:: Swagger 2
|
|
:sync: Swagger 2
|
|
|
|
In the Swagger 2 specification, you can define the name of your body. Connexion will pass
|
|
the body to your function using this name.
|
|
|
|
.. code-block:: yaml
|
|
:caption: **swagger.yaml**
|
|
|
|
paths:
|
|
/path
|
|
post:
|
|
consumes:
|
|
- application/json
|
|
parameters:
|
|
- in: body
|
|
name: payload
|
|
schema:
|
|
...
|
|
|
|
.. code-block:: python
|
|
:caption: **api.py**
|
|
|
|
def foo_get(payload)
|
|
...
|
|
|
|
Form data
|
|
`````````
|
|
|
|
In Swagger 2, form data is defined as parameters in your specification, and Connexion
|
|
passes these parameters individually:
|
|
|
|
|
|
.. code-block:: yaml
|
|
:caption: **swagger.yaml**
|
|
|
|
paths:
|
|
/path
|
|
post:
|
|
operationId: api.foo_get
|
|
consumes:
|
|
- application/json
|
|
parameters:
|
|
- in: formData
|
|
name: field1
|
|
type: string
|
|
- in: formData
|
|
name: field2
|
|
type: string
|
|
|
|
.. code-block:: python
|
|
:caption: **api.py**
|
|
|
|
def foo_get(field1, field2)
|
|
...
|
|
|
|
Connexion will not automatically pass in the default values defined in your ``requestBody``
|
|
definition, but you can activate this by configuring a different
|
|
:ref:`RequestBodyValidator<validation:Custom validators>`.
|
|
|
|
Optional arguments & Defaults
|
|
-----------------------------
|
|
|
|
If a default value is defined for a parameter in the OpenAPI specification, Connexion will
|
|
automatically pass it in if no value was included in the request. If a default is defined in the
|
|
specification, you should not define a default in your Python function, as it will never be
|
|
triggered.
|
|
|
|
If an endpoint parameter is optional and no default is defined in the specification, you should
|
|
make sure the corresponding argument is optional in your Python function as well, by assigning a
|
|
default value:
|
|
|
|
.. code-block:: python
|
|
:caption: **api.py**
|
|
|
|
def foo_get(optional_argument=None)
|
|
...
|
|
|
|
Missing arguments and kwargs
|
|
----------------------------
|
|
|
|
Connexion will inspect your function signature and only pass in the arguments that it defines. If
|
|
an argument is defined in your specification, but not in your function, Connexion will ignore it.
|
|
|
|
If you do define a ``**kwargs`` argument in your function signature, Connexion will pass in all
|
|
arguments, and the ones not explicitly defined in your signature will be collected in the
|
|
``kwargs`` argument.
|
|
|
|
Parameter Name Sanitation
|
|
-------------------------
|
|
|
|
The names of query and form parameters, as well as the name of the body
|
|
parameter are sanitized by removing characters that are not allowed in Python
|
|
symbols. I.e. all characters that are not letters, digits or the underscore are
|
|
removed, and finally characters are removed from the front until a letter or an
|
|
underscore is encountered. As an example:
|
|
|
|
.. code-block:: python
|
|
|
|
>>> re.sub('^[^a-zA-Z_]+', '', re.sub('[^0-9a-zA-Z_]', '', '$top'))
|
|
'top'
|
|
|
|
|
|
Pythonic parameters
|
|
-------------------
|
|
|
|
You can activate Pythonic parameters by setting the ``pythonic_params`` option to ``True`` on
|
|
either the application or the API:
|
|
|
|
.. tab-set::
|
|
|
|
.. tab-item:: AsyncApp
|
|
:sync: AsyncApp
|
|
|
|
.. code-block:: python
|
|
:caption: **app.py**
|
|
|
|
from connexion import AsyncApp
|
|
|
|
app = AsyncApp(__name__, pythonic_params=True)
|
|
app.add_api("openapi.yaml", pythonic_params=True)
|
|
|
|
|
|
.. tab-item:: FlaskApp
|
|
:sync: FlaskApp
|
|
|
|
.. code-block:: python
|
|
:caption: **app.py**
|
|
|
|
from connexion import FlaskApp
|
|
|
|
app = FlaskApp(__name__, pythonic_params=True)
|
|
app.add_api("openapi.yaml", pythonic_params=True):
|
|
|
|
.. tab-item:: ConnexionMiddleware
|
|
:sync: ConnexionMiddleware
|
|
|
|
.. code-block:: python
|
|
:caption: **app.py**
|
|
|
|
from asgi_framework import App
|
|
from connexion import ConnexionMiddleware
|
|
|
|
app = App(__name__)
|
|
app = ConnexionMiddleware(app, pythonic_params=True)
|
|
app.add_api("openapi.yaml", pythonic_params=True)
|
|
|
|
This does two things:
|
|
|
|
* *CamelCase* arguments are converted to *snake_case*
|
|
* If the argument name matches a Python builtin, an underscore is appended.
|
|
|
|
When ``pythonic_params`` is activated, the following specification:
|
|
|
|
.. tab-set::
|
|
|
|
.. tab-item:: OpenAPI 3
|
|
:sync: OpenAPI 3
|
|
|
|
.. code-block:: yaml
|
|
:caption: **openapi.yaml**
|
|
|
|
paths:
|
|
/foo:
|
|
get:
|
|
operationId: api.foo_get
|
|
parameters:
|
|
- name: filter
|
|
description: Some filter.
|
|
in: query
|
|
schema:
|
|
type: string
|
|
required: true
|
|
- name: FilterOption
|
|
description: Some filter option.
|
|
in: query
|
|
schema:
|
|
type: string
|
|
|
|
.. tab-item:: Swagger 2
|
|
:sync: Swagger 2
|
|
|
|
.. code-block:: yaml
|
|
:caption: **swagger.yaml**
|
|
|
|
paths:
|
|
/foo:
|
|
get:
|
|
operationId: api.foo_get
|
|
parameters:
|
|
- name: filter
|
|
description: Some filter.
|
|
in: query
|
|
type: string
|
|
required: true
|
|
- name: FilterOption
|
|
description: Some filter option.
|
|
in: query
|
|
type: string
|
|
|
|
Maps to the following Python function:
|
|
|
|
.. code-block:: python
|
|
:caption: **api.py**
|
|
|
|
def foo_get(filter_, filter_option=None):
|
|
...
|
|
|
|
Type casting
|
|
------------
|
|
|
|
Whenever possible Connexion will try to parse your argument values and cast them to the correct
|
|
Python type:
|
|
|
|
+--------------+-------------+
|
|
| OpenAPI Type | Python Type |
|
|
| | |
|
|
+==============+=============+
|
|
| integer | int |
|
|
+--------------+-------------+
|
|
| string | str |
|
|
+--------------+-------------+
|
|
| number | float |
|
|
+--------------+-------------+
|
|
| boolean | bool |
|
|
+--------------+-------------+
|
|
| array | list |
|
|
+--------------+-------------+
|
|
| object | dict |
|
|
+--------------+-------------+
|
|
| null | None |
|
|
+--------------+-------------+
|
|
|
|
Parameter serialization
|
|
-----------------------
|
|
|
|
Array and object parameters need to be serialized into lists and dicts.
|
|
|
|
.. tab-set::
|
|
|
|
.. tab-item:: OpenAPI 3
|
|
:sync: OpenAPI 3
|
|
|
|
The `OpenAPI 3 specification`_ defines the `style` and `explode` keywords which specify how
|
|
these parameters should be serialized.
|
|
|
|
To handle these, Connexion provides the ``OpenAPIUriParser`` class, which is enabled by
|
|
default when using an OpenAPI 3 spec.
|
|
|
|
Not all combinations of `style` and `explode` are supported yet. Please open an `issue`_ if
|
|
you run into any problems.
|
|
|
|
.. tab-item:: Swagger 2
|
|
:sync: Swagger 2
|
|
|
|
The `Swagger 2 specification`_ defines the `collectionFormat` keyword to specify how
|
|
these parameters should be serialized.
|
|
|
|
To handle this for you, Connexion provides the ``Swagger2URIParser`` class, which is
|
|
enabled by default when using a Swagger 2 spec. It currently supports the `pipes`, `csv`,
|
|
and `multi` collection formats.
|
|
|
|
This parser adheres to the Swagger 2.0 spec, and will only join together multiple instance
|
|
of the same query parameter if the collectionFormat is set to `multi`. Query parameters
|
|
are parsed from left to right, so if a query parameter is defined twice, then the
|
|
right-most definition wins. For example, if you provided a URI with the query string
|
|
``?letters=a,b,c&letters=d,e,f`` and ``collectionFormat: csv``, then connexion will set
|
|
``letters = ['d', 'e', 'f']``.
|
|
|
|
Connexion also provides two alternative parsers:
|
|
|
|
* The ``FirstValueURIParser``, which behaves like the ``Swagger2URIParser``, except that it
|
|
prefers the first defined value.
|
|
* The ``AlwaysMultiURIParser``, which behaves like the ``Swagger2URIParser``, except that
|
|
it always joins together multiple instances of the same query parameter.
|
|
|
|
Context
|
|
-------
|
|
|
|
Connexion can pass in some additional context. By default, this contains the following information:
|
|
|
|
.. code-block:: python
|
|
|
|
{
|
|
"api_base_path": ... # The base path of the matched API
|
|
"operation_id": ... # The operation id of matched operation
|
|
"user": ... # User information from authentication
|
|
"token_info": ... # Token information from authentication
|
|
}
|
|
|
|
Third party or custom middleware might add additional fields to this.
|
|
|
|
To receive this in your function, you can either:
|
|
|
|
* Specify the ``context_`` argument in your function signature, and the context dict will be
|
|
passed in as a whole:
|
|
|
|
.. code-block:: python
|
|
:caption: **api.py**
|
|
|
|
def foo_get(context_):
|
|
...
|
|
|
|
* Specify the keys individually in your function signature:
|
|
|
|
.. code-block:: python
|
|
:caption: **api.py**
|
|
|
|
def foo_get(user, token_info):
|
|
...
|
|
|
|
Request object
|
|
--------------
|
|
|
|
Connexion also exposes a ``Request`` class which holds all the information about the incoming
|
|
request.
|
|
|
|
.. code-block:: python
|
|
|
|
from connexion import request
|
|
|
|
.. dropdown:: View a detailed reference of the ``connexion.request`` class
|
|
:icon: eye
|
|
|
|
.. autoclass:: connexion.lifecycle.ConnexionRequest
|
|
:members:
|
|
:undoc-members:
|
|
:inherited-members:
|
|
|
|
.. _Frameworks: https://github.com/spec-first/connexion/tree/main/examples/frameworks
|
|
.. _OpenAPI 3 specification: https://swagger.io/docs/specification/serialization
|
|
.. _Swagger 2 specification: https://swagger.io/docs/specification/2-0/describing-parameters/#array
|
|
.. _issue: https://github.com/spec-first/connexion/issues |