Skip to content

Commit

Permalink
Add interface to add WSGI middleware (#1814)
Browse files Browse the repository at this point in the history
As discussed in #1807.

Allowing the injection of WSGI middleware can enable easier migration
from Connexion 2 to Connexion 3. The use cases are limited though, as
this will only work for middleware that can work at the end of the
middleware stack.
  • Loading branch information
RobbeSneyders committed Nov 19, 2023
1 parent 563fbf8 commit 14e02fa
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 4 deletions.
18 changes: 17 additions & 1 deletion connexion/apps/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from connexion.operations import AbstractOperation
from connexion.options import SwaggerUIOptions
from connexion.resolver import Resolver
from connexion.types import MaybeAwaitable
from connexion.types import MaybeAwaitable, WSGIApp
from connexion.uri_parsing import AbstractURIParser


Expand Down Expand Up @@ -259,3 +259,19 @@ def add_error_handler(
],
) -> None:
self.middleware.add_error_handler(code_or_exception, function)

def add_wsgi_middleware(
self, middleware: t.Type[WSGIApp], **options: t.Any
) -> None:
"""Wrap the underlying Flask application with a WSGI middleware. Note that it will only be
called at the end of the middleware stack. Middleware that needs to act sooner, needs to
be added as ASGI middleware instead.
Adding multiple middleware using this method wraps each middleware around the previous one.
:param middleware: Middleware class to add
:param options: Options to pass to the middleware_class on initialization
"""
self._middleware_app.asgi_app.app = middleware(
self._middleware_app.asgi_app.app, **options # type: ignore
)
32 changes: 30 additions & 2 deletions connexion/types.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
import types
import typing as t

ReturnType = t.TypeVar("ReturnType")
MaybeAwaitable = t.Union[t.Awaitable[ReturnType], ReturnType]
# Maybe Awaitable
_ReturnType = t.TypeVar("_ReturnType")
MaybeAwaitable = t.Union[t.Awaitable[_ReturnType], _ReturnType]

# WSGIApp
Environ = t.Mapping[str, object]

_WriteCallable = t.Callable[[bytes], t.Any]
_ExcInfo = t.Tuple[type, BaseException, types.TracebackType]

_StartResponseCallable = t.Callable[
[
str, # status
t.Sequence[t.Tuple[str, str]], # response headers
],
_WriteCallable, # write() callable
]
_StartResponseCallableWithExcInfo = t.Callable[
[
str, # status
t.Sequence[t.Tuple[str, str]], # response headers
t.Optional[_ExcInfo], # exc_info
],
_WriteCallable, # write() callable
]
StartResponse = t.Union[_StartResponseCallable, _StartResponseCallableWithExcInfo]
ResponseStream = t.Iterable[bytes]

WSGIApp = t.Callable[[Environ, StartResponse], ResponseStream]
16 changes: 15 additions & 1 deletion docs/middleware.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ You can easily add additional ASGI middleware to the middleware stack with the
.. automethod:: connexion.FlaskApp.add_middleware
:noindex:

You can also add WSGI middleware to a ``FlaskApp``. Note that it will only be called at the
end of the middleware stack. If you need your middleware to act sooner, you will have to
use an ASGI middleware instead.

.. code-block:: python
app.add_wsgi_middleware(MiddlewareClass, **options)
.. dropdown:: View a detailed reference of the :code:`add_middleware` method
:icon: eye

.. automethod:: connexion.FlaskApp.add_wsgi_middleware
:noindex:

.. tab-item:: ConnexionMiddleware
:sync: ConnexionMiddleware

Expand All @@ -77,7 +91,7 @@ You can easily add additional ASGI middleware to the middleware stack with the
app.add_middleware(MiddlewareClass, **options)
.. dropdown:: View a detailed reference of the :code:`add_middleware` method
.. dropdown:: View a detailed reference of the :code:`add_wsgi_middleware` method
:icon: eye

.. automethod:: connexion.ConnexionMiddleware.add_middleware
Expand Down
28 changes: 28 additions & 0 deletions tests/test_middleware.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import typing as t
from unittest.mock import Mock

import pytest
from connexion import FlaskApp
from connexion.middleware import ConnexionMiddleware, MiddlewarePosition
from connexion.middleware.swagger_ui import SwaggerUIMiddleware
from connexion.types import Environ, ResponseStream, StartResponse, WSGIApp
from starlette.datastructures import MutableHeaders

from conftest import build_app_from_fixture
Expand Down Expand Up @@ -81,3 +86,26 @@ def test_position(spec, app_class):
== f"Could not insert middleware at position BEFORE_SWAGGER. "
f"Please make sure you have a {SwaggerUIMiddleware} in your stack."
)


def test_add_wsgi_middleware(spec):
app: FlaskApp = build_app_from_fixture("simple", app_class=FlaskApp, spec_file=spec)

class WSGIMiddleware:
def __init__(self, app_: WSGIApp, mock_counter):
self.next_app = app_
self.mock_counter = mock_counter

def __call__(
self, environ: Environ, start_response: StartResponse
) -> ResponseStream:
self.mock_counter()
return self.next_app(environ, start_response)

mock = Mock()
app.add_wsgi_middleware(WSGIMiddleware, mock_counter=mock)

app_client = app.test_client()
app_client.post("/v1.0/greeting/robbe")

mock.assert_called_once()

0 comments on commit 14e02fa

Please sign in to comment.