mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-06 04:19:26 +00:00
@@ -16,6 +16,7 @@ from connexion.decorators.parameter import (
|
||||
from connexion.decorators.response import (
|
||||
AsyncResponseDecorator,
|
||||
BaseResponseDecorator,
|
||||
NoResponseDecorator,
|
||||
SyncResponseDecorator,
|
||||
)
|
||||
from connexion.frameworks.abstract import Framework
|
||||
@@ -94,10 +95,12 @@ class BaseDecorator:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class FlaskDecorator(BaseDecorator):
|
||||
"""Decorator for usage with Flask. The parameter decorator works with a Flask request,
|
||||
and provides Flask datastructures to the view function. The response decorator returns
|
||||
a Flask response"""
|
||||
class WSGIDecorator(BaseDecorator):
|
||||
"""Decorator for usage with WSGI apps. The parameter decorator works with a Flask request,
|
||||
and provides Flask datastructures to the view function. This works for any WSGI app, since
|
||||
we get the request via the connexion context provided by WSGI middleware.
|
||||
|
||||
This decorator does not parse responses, but passes them directly to the WSGI App."""
|
||||
|
||||
framework = FlaskFramework
|
||||
|
||||
@@ -106,8 +109,8 @@ class FlaskDecorator(BaseDecorator):
|
||||
return SyncParameterDecorator
|
||||
|
||||
@property
|
||||
def _response_decorator_cls(self) -> t.Type[SyncResponseDecorator]:
|
||||
return SyncResponseDecorator
|
||||
def _response_decorator_cls(self) -> t.Type[BaseResponseDecorator]:
|
||||
return NoResponseDecorator
|
||||
|
||||
@property
|
||||
def _sync_async_decorator(self) -> t.Callable[[t.Callable], t.Callable]:
|
||||
@@ -133,6 +136,17 @@ class FlaskDecorator(BaseDecorator):
|
||||
return wrapper
|
||||
|
||||
|
||||
class FlaskDecorator(WSGIDecorator):
|
||||
"""Decorator for usage with Connexion or Flask apps. The parameter decorator works with a
|
||||
Flask request, and provides Flask datastructures to the view function.
|
||||
|
||||
The response decorator returns Flask responses."""
|
||||
|
||||
@property
|
||||
def _response_decorator_cls(self) -> t.Type[SyncResponseDecorator]:
|
||||
return SyncResponseDecorator
|
||||
|
||||
|
||||
class ASGIDecorator(BaseDecorator):
|
||||
"""Decorator for usage with ASGI apps. The parameter decorator works with a Starlette request,
|
||||
and provides Starlette datastructures to the view function. This works for any ASGI app, since
|
||||
@@ -148,10 +162,6 @@ class ASGIDecorator(BaseDecorator):
|
||||
|
||||
@property
|
||||
def _response_decorator_cls(self) -> t.Type[BaseResponseDecorator]:
|
||||
class NoResponseDecorator(BaseResponseDecorator):
|
||||
def __call__(self, function: t.Callable) -> t.Callable:
|
||||
return lambda request: function(request)
|
||||
|
||||
return NoResponseDecorator
|
||||
|
||||
@property
|
||||
|
||||
@@ -6,12 +6,12 @@ import types
|
||||
import typing as t
|
||||
from enum import Enum
|
||||
|
||||
from connexion import utils
|
||||
from connexion.context import operation
|
||||
from connexion.datastructures import NoContent
|
||||
from connexion.exceptions import NonConformingResponseHeaders
|
||||
from connexion.frameworks.abstract import Framework
|
||||
from connexion.lifecycle import ConnexionResponse
|
||||
from connexion.utils import is_json_mimetype
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,27 +27,27 @@ class BaseResponseDecorator:
|
||||
|
||||
def build_framework_response(self, handler_response):
|
||||
data, status_code, headers = self._unpack_handler_response(handler_response)
|
||||
content_type = self._deduct_content_type(data, headers)
|
||||
content_type = self._infer_content_type(data, headers)
|
||||
if not self.framework.is_framework_response(data):
|
||||
data, status_code = self._prepare_body_and_status_code(
|
||||
data, status_code=status_code, mimetype=content_type
|
||||
)
|
||||
data = self._serialize_data(data, content_type=content_type)
|
||||
status_code = status_code or self._infer_status_code(data)
|
||||
headers = self._update_headers(headers, content_type=content_type)
|
||||
return self.framework.build_response(
|
||||
data, content_type=content_type, status_code=status_code, headers=headers
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _deduct_content_type(data: t.Any, headers: dict) -> str:
|
||||
"""Deduct the response content type from the returned data, headers and operation spec.
|
||||
def _infer_content_type(data: t.Any, headers: dict) -> t.Optional[str]:
|
||||
"""Infer the response content type from the returned data, headers and operation spec.
|
||||
|
||||
:param data: Response data
|
||||
:param headers: Headers returned by the handler.
|
||||
|
||||
:return: Deducted content type
|
||||
:return: Inferred content type
|
||||
|
||||
:raises: NonConformingResponseHeaders if content type cannot be deducted.
|
||||
"""
|
||||
content_type = headers.get("Content-Type")
|
||||
content_type = utils.extract_content_type(headers)
|
||||
|
||||
# TODO: don't default
|
||||
produces = list(set(operation.produces))
|
||||
@@ -66,45 +66,56 @@ class BaseResponseDecorator:
|
||||
pass
|
||||
elif len(produces) == 1:
|
||||
content_type = produces[0]
|
||||
elif isinstance(data, str) and "text/plain" in produces:
|
||||
content_type = "text/plain"
|
||||
elif (
|
||||
isinstance(data, bytes)
|
||||
or isinstance(data, (types.GeneratorType, collections.abc.Iterator))
|
||||
) and "application/octet-stream" in produces:
|
||||
content_type = "application/octet-stream"
|
||||
else:
|
||||
raise NonConformingResponseHeaders(
|
||||
"Multiple response content types are defined in the operation spec, but the "
|
||||
"handler response did not specify which one to return."
|
||||
)
|
||||
if isinstance(data, str):
|
||||
for produced_content_type in produces:
|
||||
if "text/plain" in produced_content_type:
|
||||
content_type = produced_content_type
|
||||
elif isinstance(data, bytes) or isinstance(
|
||||
data, (types.GeneratorType, collections.abc.Iterator)
|
||||
):
|
||||
for produced_content_type in produces:
|
||||
if "application/octet-stream" in produced_content_type:
|
||||
content_type = produced_content_type
|
||||
|
||||
if content_type is None:
|
||||
raise NonConformingResponseHeaders(
|
||||
"Multiple response content types are defined in the operation spec, but "
|
||||
"the handler response did not specify which one to return."
|
||||
)
|
||||
|
||||
return content_type
|
||||
|
||||
def _prepare_body_and_status_code(
|
||||
self, data, *, status_code: int = None, mimetype: str
|
||||
) -> tuple:
|
||||
if data is NoContent:
|
||||
data = None
|
||||
|
||||
if status_code is None:
|
||||
if data is None:
|
||||
status_code = 204
|
||||
else:
|
||||
status_code = 200
|
||||
|
||||
if data is not None:
|
||||
body = self._serialize_data(data, mimetype)
|
||||
else:
|
||||
body = data
|
||||
|
||||
return body, status_code
|
||||
|
||||
def _serialize_data(self, data: t.Any, mimetype: str) -> t.Any:
|
||||
if is_json_mimetype(mimetype):
|
||||
def _serialize_data(self, data: t.Any, *, content_type: str) -> t.Any:
|
||||
"""Serialize the data based on the content type."""
|
||||
if data is None or data is NoContent:
|
||||
return None
|
||||
# TODO: encode responses
|
||||
mime_type, _ = utils.split_content_type(content_type)
|
||||
if utils.is_json_mimetype(mime_type):
|
||||
return self.jsonifier.dumps(data)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _infer_status_code(data: t.Any) -> int:
|
||||
"""Infer the status code from the returned data."""
|
||||
if data is None:
|
||||
return 204
|
||||
return 200
|
||||
|
||||
@staticmethod
|
||||
def _update_headers(
|
||||
headers: t.Dict[str, str], *, content_type: str
|
||||
) -> t.Dict[str, str]:
|
||||
# Check if Content-Type is in headers, taking into account case-insensitivity
|
||||
for key, value in headers.items():
|
||||
if key.lower() == "content-type":
|
||||
return headers
|
||||
|
||||
if content_type:
|
||||
headers["Content-Type"] = content_type
|
||||
return headers
|
||||
|
||||
@staticmethod
|
||||
def _unpack_handler_response(
|
||||
handler_response: t.Union[str, bytes, dict, list, tuple]
|
||||
@@ -186,3 +197,10 @@ class AsyncResponseDecorator(BaseResponseDecorator):
|
||||
return self.build_framework_response(handler_response)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class NoResponseDecorator(BaseResponseDecorator):
|
||||
"""Dummy decorator to skip response serialization."""
|
||||
|
||||
def __call__(self, function: t.Callable) -> t.Callable:
|
||||
return lambda request: function(request)
|
||||
|
||||
@@ -40,7 +40,8 @@ class RequestValidationOperation:
|
||||
|
||||
:return: A tuple of mime type, encoding
|
||||
"""
|
||||
mime_type, encoding = utils.extract_content_type(headers)
|
||||
content_type = utils.extract_content_type(headers)
|
||||
mime_type, encoding = utils.split_content_type(content_type)
|
||||
if mime_type is None:
|
||||
# Content-type header is not required. Take a best guess.
|
||||
try:
|
||||
|
||||
@@ -38,7 +38,8 @@ class ResponseValidationOperation:
|
||||
|
||||
:return: A tuple of mime type, encoding
|
||||
"""
|
||||
mime_type, encoding = utils.extract_content_type(headers)
|
||||
content_type = utils.extract_content_type(headers)
|
||||
mime_type, encoding = utils.split_content_type(content_type)
|
||||
if mime_type is None:
|
||||
# Content-type header is not required. Take a best guess.
|
||||
try:
|
||||
|
||||
@@ -288,31 +288,56 @@ def not_installed_error(exc, *, msg=None): # pragma: no cover
|
||||
|
||||
|
||||
def extract_content_type(
|
||||
headers: t.List[t.Tuple[bytes, bytes]]
|
||||
) -> t.Tuple[t.Optional[str], t.Optional[str]]:
|
||||
headers: t.Union[t.List[t.Tuple[bytes, bytes]], t.Dict[str, str]]
|
||||
) -> t.Optional[str]:
|
||||
"""Extract the mime type and encoding from the content type headers.
|
||||
|
||||
:param headers: Headers from ASGI scope
|
||||
|
||||
:return: A tuple of mime type, encoding
|
||||
:return: The content type if available in headers, otherwise None
|
||||
"""
|
||||
mime_type, encoding = None, None
|
||||
for key, value in headers:
|
||||
content_type: t.Optional[str] = None
|
||||
|
||||
header_pairs_type = t.Collection[t.Tuple[t.Union[str, bytes], t.Union[str, bytes]]]
|
||||
header_pairs: header_pairs_type = headers.items() if isinstance(headers, dict) else headers # type: ignore
|
||||
for key, value in header_pairs:
|
||||
# Headers can always be decoded using latin-1:
|
||||
# https://stackoverflow.com/a/27357138/4098821
|
||||
decoded_key = key.decode("latin-1")
|
||||
if decoded_key.lower() == "content-type":
|
||||
content_type = value.decode("latin-1")
|
||||
if ";" in content_type:
|
||||
mime_type, parameters = content_type.split(";", maxsplit=1)
|
||||
if isinstance(key, bytes):
|
||||
decoded_key: str = key.decode("latin-1")
|
||||
else:
|
||||
decoded_key = key
|
||||
|
||||
prefix = "charset="
|
||||
for parameter in parameters.split(";"):
|
||||
if parameter.startswith(prefix):
|
||||
encoding = parameter[len(prefix) :]
|
||||
if decoded_key.lower() == "content-type":
|
||||
if isinstance(value, bytes):
|
||||
content_type = value.decode("latin-1")
|
||||
else:
|
||||
mime_type = content_type
|
||||
content_type = value
|
||||
break
|
||||
|
||||
return content_type
|
||||
|
||||
|
||||
def split_content_type(
|
||||
content_type: t.Optional[str],
|
||||
) -> t.Tuple[t.Optional[str], t.Optional[str]]:
|
||||
"""Split the content type in mime_type and encoding. Other parameters are ignored."""
|
||||
mime_type, encoding = None, None
|
||||
|
||||
if content_type is None:
|
||||
return mime_type, encoding
|
||||
|
||||
# Check for parameters
|
||||
if ";" in content_type:
|
||||
mime_type, parameters = content_type.split(";", maxsplit=1)
|
||||
|
||||
# Find parameter describing the charset
|
||||
prefix = "charset="
|
||||
for parameter in parameters.split(";"):
|
||||
if parameter.startswith(prefix):
|
||||
encoding = parameter[len(prefix) :]
|
||||
else:
|
||||
mime_type = content_type
|
||||
return mime_type, encoding
|
||||
|
||||
|
||||
|
||||
@@ -43,13 +43,16 @@ Automatic parameter handling
|
||||
To activate this behavior when using the ``ConnexionMiddleware`` wrapping a third party
|
||||
application, you can leverage the following decorators provided by Connexion:
|
||||
|
||||
* FlaskDecorator: provides automatic parameter injection and response serialization for
|
||||
* ``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
|
||||
* ``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
|
||||
* ``StarletteDecorator``: provides automatic parameter injection and response serialization
|
||||
for Starlette applications.
|
||||
|
||||
.. code-block:: python
|
||||
@@ -57,6 +60,7 @@ Automatic parameter handling
|
||||
|
||||
from asgi_framework import App
|
||||
from connexion import ConnexionMiddleware
|
||||
from connexion.decorators import ASGIDecorator
|
||||
|
||||
@app.route("/greeting/<name>", methods=["POST"])
|
||||
@ASGIDecorator()
|
||||
|
||||
@@ -1,97 +1,185 @@
|
||||
Response Handling
|
||||
=================
|
||||
|
||||
When your application returns a response, Connexion provides the following functionality based on
|
||||
your OpenAPI spec:
|
||||
|
||||
- It automatically translates Python errors into HTTP problem responses (see :doc:`exceptions`)
|
||||
- It automatically serializes the response for certain content types
|
||||
- It validates the response body and headers (see :doc:`validation`)
|
||||
|
||||
On this page, we zoom in on the response serialization.
|
||||
|
||||
Response Serialization
|
||||
----------------------
|
||||
|
||||
.. tab-set::
|
||||
|
||||
.. tab-item:: AsyncApp
|
||||
:sync: AsyncApp
|
||||
|
||||
|
||||
When working with Connexion, you can return ordinary Python types, and connexion will serialize
|
||||
them into a network response.
|
||||
|
||||
.. tab-item:: FlaskApp
|
||||
:sync: FlaskApp
|
||||
|
||||
When working with Connexion, you can return ordinary Python types, and connexion will serialize
|
||||
them into a network response.
|
||||
|
||||
.. tab-item:: ConnexionMiddleware
|
||||
:sync: ConnexionMiddleware
|
||||
|
||||
When working with Connexion, you can return ordinary Python types, and connexion will serialize
|
||||
them into a network response.
|
||||
|
||||
To activate this behavior when using the ``ConnexionMiddleware`` wrapping a third party
|
||||
application, you can leverage the following decorators provided by Connexion:
|
||||
|
||||
* ``FlaskDecorator``: provides automatic parameter injection and response serialization for
|
||||
Flask applications.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **app.py**
|
||||
|
||||
from connexion import ConnexionMiddleware
|
||||
from connexion.decorators import FlaskDecorator
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
app = ConnexionMiddleware(app)
|
||||
app.add_api("openapi.yaml")
|
||||
|
||||
@app.route("/endpoint")
|
||||
@FlaskDecorator()
|
||||
def endpoint(name):
|
||||
...
|
||||
|
||||
* ``StarletteDecorator``: provides automatic parameter injection and response serialization
|
||||
for Starlette applications.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **app.py**
|
||||
|
||||
from connexion import ConnexionMiddleware
|
||||
from connexion.decorators import StarletteDecorator
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Route
|
||||
|
||||
@StarletteDecorator()
|
||||
def endpoint(name):
|
||||
...
|
||||
|
||||
app = Starlette(routes=[Route('/endpoint', endpoint)])
|
||||
app = ConnexionMiddleware(app)
|
||||
app.add_api("openapi.yaml")
|
||||
|
||||
For a full example, see our `Frameworks`_ example.
|
||||
|
||||
The generic ``connexion.decorators.WSGIDecorator`` and
|
||||
``connexion.decorators.ASGIDecorator`` unfortunately don't support response
|
||||
serialization, but you can extend them to implement your own decorator for a specific
|
||||
WSGI or ASGI framework respectively.
|
||||
|
||||
.. note::
|
||||
|
||||
If you implement a custom decorator, and think it would be valuable for other users, we
|
||||
would appreciate it as a contribution.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **api.py**
|
||||
|
||||
def endpoint():
|
||||
data = "success"
|
||||
status_code = 200
|
||||
headers = {"Content-Type": "text/plain}
|
||||
return data, status_code, headers
|
||||
|
||||
Data
|
||||
````
|
||||
|
||||
If your API returns responses with the ``application/json`` content type, you can return
|
||||
a simple ``dict`` or ``list`` and Connexion will serialize (``json.dumps``) the data for you.
|
||||
|
||||
**Customizing JSON serialization**
|
||||
|
||||
Connexion allows you to customize the ``Jsonifier`` used to serialize json data by subclassing the
|
||||
``connexion.jsonifier.Jsonifier`` class and passing it when instantiating your app or registering
|
||||
an API:
|
||||
|
||||
.. tab-set::
|
||||
|
||||
.. tab-item:: AsyncApp
|
||||
:sync: AsyncApp
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **app.py**
|
||||
|
||||
from connexion import AsyncApp
|
||||
|
||||
app = AsyncApp(__name__, jsonifier=)
|
||||
app.add_api("openapi.yaml", jsonifier=c)
|
||||
|
||||
|
||||
.. tab-item:: FlaskApp
|
||||
:sync: FlaskApp
|
||||
|
||||
.. code-block:: python
|
||||
:caption: **app.py**
|
||||
|
||||
from connexion import FlaskApp
|
||||
|
||||
app = FlaskApp(__name__, jsonifier=...)
|
||||
app.add_api("openapi.yaml", jsonifier=...):
|
||||
|
||||
.. 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, jsonifier=...)
|
||||
app.add_api("openapi.yaml", jsonifier=...)
|
||||
|
||||
Status code
|
||||
```````````
|
||||
|
||||
If no status code is provided, Connexion will automatically set it as ``200`` if data is
|
||||
returned, or as ``204`` if ``None`` or ``connexion.datastructures.NoContent`` is returned.
|
||||
|
||||
Headers
|
||||
```````
|
||||
|
||||
The headers can be used to define any response headers to return. If your OpenAPI specification
|
||||
defines multiple responses with different content types, you can explicitly set the
|
||||
``Content-Type`` header to tell Connexion which response to validate against.
|
||||
|
||||
If you do not explicitly return a ``Content-Type`` header, Connexion's behavior depends on the
|
||||
Responses defined in your OpenAPI spec:
|
||||
|
||||
* If you have defined a single response content type in your OpenAPI specification, Connexion
|
||||
will automatically set it.
|
||||
* If you have defined multiple response content types in your OpenAPI specification, Connexion
|
||||
will try to infer which one matches your response and set it. If it cannot infer the content
|
||||
type, an error is raised.
|
||||
* If you have not defined a response content type in your OpenAPI specification, Connexion will
|
||||
automatically set it to ``application/json`` unless you don't return any data. This is mostly
|
||||
because of backward-compatibility, and can be circumvented easily by defining a response
|
||||
content type in your OpenAPI specification.
|
||||
|
||||
Skipping response serialization
|
||||
-------------------------------
|
||||
|
||||
If your endpoint returns an instance of ``connexion.lifecycle.ConnexionResponse``, or a
|
||||
framework-specific response (``flask.Response`` or ``starlette.responses.Response``), response
|
||||
serialization is skipped, and the response is passed directly to the underlying framework.
|
||||
|
||||
If your endpoint returns a `Response`
|
||||
If the endpoint returns a `Response` object this response will be used as is.
|
||||
|
||||
Otherwise, and by default and if the specification defines that an endpoint
|
||||
produces only JSON, connexion will automatically serialize the return value
|
||||
for you and set the right content type in the HTTP header.
|
||||
|
||||
If the endpoint produces a single non-JSON mimetype then Connexion will
|
||||
automatically set the right content type in the HTTP header.
|
||||
|
||||
Customizing JSON encoder
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Connexion allows you to customize the `JSONEncoder` class in the Flask app
|
||||
instance `json_encoder` (`connexion.App:app`). If you wanna reuse the
|
||||
Connexion's date-time serialization, inherit your custom encoder from
|
||||
`connexion.apps.flask_app.FlaskJSONEncoder`.
|
||||
|
||||
For more information on the `JSONEncoder`, see the `Flask documentation`_.
|
||||
|
||||
.. _Flask Documentation: https://flask.palletsprojects.com/en/2.0.x/api/#flask.json.JSONEncoder
|
||||
|
||||
Returning status codes
|
||||
----------------------
|
||||
There are two ways of returning a specific status code.
|
||||
|
||||
One way is to return a `Response` object that will be used unchanged.
|
||||
|
||||
The other is returning it as a second return value in the response. For example
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def my_endpoint():
|
||||
return 'Not Found', 404
|
||||
|
||||
Returning Headers
|
||||
-----------------
|
||||
There are two ways to return headers from your endpoints.
|
||||
|
||||
One way is to return a `Response` object that will be used unchanged.
|
||||
|
||||
The other is returning a dict with the header values as the third return value
|
||||
in the response:
|
||||
|
||||
For example
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def my_endpoint():
|
||||
return 'Not Found', 404, {'x-error': 'not found'}
|
||||
|
||||
|
||||
Response Validation
|
||||
-------------------
|
||||
While, by default Connexion doesn't validate the responses it's possible to
|
||||
do so by opting in when adding the API:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import connexion
|
||||
|
||||
app = connexion.FlaskApp(__name__, specification_dir='swagger/')
|
||||
app.add_api('my_api.yaml', validate_responses=True)
|
||||
app.run(port=8080)
|
||||
|
||||
This will validate all the responses using `jsonschema` and is specially useful
|
||||
during development.
|
||||
|
||||
|
||||
Custom Validator
|
||||
-----------------
|
||||
|
||||
By default, response body contents are validated against OpenAPI schema
|
||||
via ``connexion.decorators.response.ResponseValidator``, if you want to change
|
||||
the validation, you can override the default class with:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
validator_map = {
|
||||
'response': CustomResponseValidator
|
||||
}
|
||||
app = connexion.FlaskApp(__name__)
|
||||
app.add_api('api.yaml', ..., validator_map=validator_map)
|
||||
|
||||
|
||||
Error Handling
|
||||
--------------
|
||||
By default connexion error messages are JSON serialized according to
|
||||
`Problem Details for HTTP APIs`_
|
||||
|
||||
Application can return errors using ``connexion.problem``.
|
||||
|
||||
.. _Problem Details for HTTP APIs: https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
|
||||
.. _Frameworks: https://github.com/spec-first/connexion/tree/main/examples/frameworks
|
||||
|
||||
@@ -81,7 +81,7 @@ def test_produce_decorator(simple_app):
|
||||
app_client = simple_app.test_client()
|
||||
|
||||
get_bye = app_client.get("/v1.0/bye/jsantos")
|
||||
assert get_bye.headers.get("content-type") == "text/plain; charset=utf-8"
|
||||
assert get_bye.headers.get("content-type", "").startswith("text/plain")
|
||||
|
||||
|
||||
def test_returning_response_tuple(simple_app):
|
||||
|
||||
Reference in New Issue
Block a user