Repository: aio-libs/aiohttp

Issue:
Middleware factory is run for every single request
## Long story short
Maybe that's just my unwaranted assumption, but I thought that a middleware factory should run once for every handler, creating the wrapped handlers. Turns out, it's run for every request. This seems to be terribly inefficient.

Is this an expected behaviour, something that I'm seeing for some strange reasons, or a regression in the library?

## Expected behaviour
Run each middleware factory for each handler at one point, at the start of application's lifecycle. Let the new handlers handle requests from that point without being constantly recreated.
   
## Actual behaviour
Middleware factory is run for every request to a handler.

## Steps to reproduce
What I'm creating is a middleware that will parse JWT tokens. I need a mechanism to exculde this parsing on some handlers, since some will be accesses without authorization. I didn't use auihttp_security, since it imposes structure that I don't need (I don't have sessions, for example). I may open-source this code later.

The important bits of my code (with some alterations):

```python
# authorization.py

from functools import wraps

from aiohttp import web
import jwt

NON_SECURED_HANDLERS_KEY = "set of endpoints that don't expect a token"
USER_ID_KEY = 'user ID given by token middleware'

async def token_reading_middleware(app, handler):
    """Reads the token from the authorization header and determines the user ID based on the
    scope inside it.

    Raises 401 if there's something wrong with the header or it's missing.
    
    This check can be disabled by adding the function to the set of non secured handlers
    (on the application object, under `NON_SECURED_HANDLERS_KEY`).
    """
    # assume that APP_CONFIG is imported
    app_secret = app[APP_CONFIG].app_secret

    @wraps(handler)
    async def middleware_handler(request):
        """Adds the user ID parsed from the authorization header to request or raises 401 error.
        """
        # let's say that this function is implemented somewhere and returns 401 is something's wrong with the header
        user_id = _get_user_id_from_auth_header(request, app_secret)
        request[USER_ID_KEY] = user_id
        return await handler(request)

    # this is to tell us that the factory code has been run
    print('XXX', flush=True)
    
    # let's say that this function determines if the handler is esxccepted from security or not
    if _is_except_from_security(handler, app):
        return handler
    return middleware_handler

# app.py
from aiohttp import web

async def _health_check(_):
    return web.HTTPNoContent()


loop =  asyncio.get_event_loop()
app = web.Application(
    loop=loop,
    middlewares=[authorization.token_reading_middleware])
app.router.add_route('GET','/', _health_check)

web.run_app(
    app,
    host='0.0.0.0',
    port=8080,
    loop=loop,
)
```

After starting the app (`python app.py`) I call http://localhost:8080 consecutively. I'm getting 401s because I don;t pass the authorization header.

The problem is, that he application keeps printing 'XXX' for every request, to I know that it jumps through all the hoops of preparing the wrapper and wrapping the handler every time.

## Your environment
AioHTTP 2.2.5
Python 3.6.2
Kubuntu 16.04.3 LTS x64


Discussion:
The behavior is intentional, it's here starting from very first middleware support implementation.
Do you have numbers how slow current implementation is and how it could be improved?
I'm slightly shocked by this; until I read this issue I had always assumed factories where called once, not for every request. I never even thought to check.

If they're called for every request, what's the point in having the outer coroutine?

I've just looked now at a project I'm working on, almost every middleware has processing outside the "handler" which I had assumed would only be called once. None of this processing will take very long so when testing or deploying with small or moderate usage it wouldn't be obvious, but if performance becomes important this could have been a problem.

Apparently this surprising behaviour isn't [documented](http://aiohttp.readthedocs.io/en/stable/web.html#middlewares). (or am I being blind?)

Solutions (in order of my preference):
1. call middleware factories once when freezing the app.
2. remove the outer coroutine so middlewares are just simple coroutines.
3. add a big fat prominent warning to the docs that the behaviour of middleware is not intuitive. 

I'm guessing 1) is too big a change to be accepted? (My guess would be, 90%+ of developers assume this behaviour anyway, so it shouldn't cause problems)

Perhaps 2) would be possible if current behaviour was allowed but just raises a depreciation warning for a few releases?
Looks like the point of running the middlewares each time to thread the actual request handler through the middleware chain, so number 1 can't be done. 

As for number 2, I'd imagine that's so any middleware setup that needs to do IO doesn't block. 

Number 3 is probably the best, unless the outer coroutine could be deprecated. 
There is closed PR for composing middlewares chain only once: #565 

I'm not convinced if we should optimize this code unless benchmark will show significant performance degradation by chain creation.
@asvetlov As for the measurements of performance and implementation suggestions, I'll try to prepare something today. Gotta look at the closed pr as well.
> I'm not convinced if we should optimize this code unless benchmark will show significant performance degradation by chain creation.

I wouldn't expect it to make any difference under normal conditions (and it doesn't, see below), the point is that their's a risk developers assume the factories are only called once and therefore wrongly call setup code in the outer coroutine. 

This is a particularly big problem if people release helper packages implemented as middleware where some setup is required, because in this case there's no way at all to add initialisation logic when just adding middleware.

I've tried removing the factory:

```python
        if resp is None:
            handler = match_info.handler
            for app in match_info.apps[::-1]:
                # for factory in app._middlewares:
                #     handler = yield from factory(app, handler)
                for m in app._middlewares:
                    handler = partial(m, handler=handler)

            resp = yield from handler(request)
```

so middleware is defined as just a simple coroutine:

```python
async def my_middleware(request, handler):
    return await handler(request)
```

it seems to has no effect on performance.
Initial idea for middleware was that middleware itself looks like regular handler: it accepts request and returns response instance (or raises http exception).
To process middlewares chain there is a need for middleware factory which accepts *next* handler (doesn't matter is it another middleware or final web handler) and returns a new middleware instance.
Proposal from @samuelcolvin works pretty well but the ship has sailed, I don't sure if we need adding new api with existing approach deprecation without any benefit.

Regarding to docs update for more explicit describing current behavior -- I very appreciate it.
It would be pretty easy to allow "old style middleware" and just raise a warning but process it as we do now.

I completely understand what you mean by "without any benefit" (no bugs fixed, no new feature), but I think there are some benefits:
1. The new style is cleaner, less code, quicker to read, quicker to write.
2. As I said before the old style leaves a big bad bear trap for developers to fall into: assuming the outer coroutine is only called once and using it for setup code. 

Perhaps we should add a note to the docs and leave it for now. If other developers admit they fell into this trap, then perhaps there's sufficient reason to make the change?
@samuelcolvin I'm +0 for proposed change.
Let's do the following: you'll make a Pull Request with full implementation and test coverage.
Then we'll open a kind of voting for collecting feedback from other devs.
I like the simplicity of @samuelcolvin's solution, it's definitely a more straightforward way of getting the current functionality. But I also think, that there may be a benefit to middleware factories that are run just once when freezing the app.

I know it's not an acceptable implementation, but my POC looks like this:

```diff
diff --git a/aiohttp/web.py b/aiohttp/web.py
index 6caee4c..65f1cf4 100644
--- a/aiohttp/web.py
+++ b/aiohttp/web.py
@@ -301,8 +301,8 @@ class Application(MutableMapping):
         if resp is None:
             handler = match_info.handler
             for app in match_info.apps[::-1]:
-                for factory in app._middlewares:
-                    handler = yield from factory(app, handler)
+                # assert below needs to look at the middlewares of the last app...
+                pass
 
             resp = yield from handler(request)
 
@@ -446,6 +446,16 @@ def run_app(app, *, host=None, port=None, path=None, sock=None,
                 # add_signal_handler is not implemented on Windows
                 pass
 
+        # I know I'm doing that for only one app, which won't fly with subapps.
+        # This is just a hack to show my thinking.
+        for resource in app.router.resources():
+            # another dirty hack; implementing this whole thing properly will probably take some
+            # thinking about interfaces of a few classes.
+            http_handler = resource._routes[0]._handler
+            for factory in app._middlewares:
+                http_handler = loop.run_until_complete(factory(app, http_handler))
+            resource._routes[0]._handler = http_handler
+
         try:
             print("======== Running on {} ========\n"
                   "(Press CTRL+C to quit)".format(', '.join(uris)))

```

Available [here](https://github.com/butla/aiohttp/commit/3ab39dd9752d7e130d9aae87f22eae0cacbdb992).

For my application (and middleware factory), the app can respond with 401 about twice as fast: 2488 requests/second on commit [d2236cfe](https://github.com/aio-libs/aiohttp/tree/d2236cfe75ccd518c684a1b3f3a6a2c8da0267a1) (the last one from master) vs 4090 reqs/sec on [3ab39dd97](https://github.com/butla/aiohttp/tree/3ab39dd9752d7e130d9aae87f22eae0cacbdb992) (mine)

It'll matter when the middleware factory does a bit more complex setup, or calls something over the network, e.g. get some public keys to verify JWTs.

Right now, with the current implementation of middlewares, I'll be doing that this one-time preparation outside of the framework, so I'll have a factory, that produces a factory, that produces the wrapped handler. Smells like corporate Java :)

I know that changing the way it works right now can potentially break someones code, so we totally shouldn't do it. If we'd be going with my approach, we can introduce a new parameter - `middleware_factories` that would be mutually exclusive with `middlewares`. As for the approach with streamlined middleware coroutines, I don't know what be a good name for the parameter. But I also think that setting it should turn off the old middlewares.
> It'll matter when the middleware factory does a bit more complex setup

You should be doing this in startup coroutines and attaching the result to the app with `app['public_keys'] = await get_public_keys()`. That will avoid calling the logic after startup and will be easier to understand for anyone else who looks at your app.
Yeah, that's one way of doing it. The thing about being easy to understand depends on the specific cases, I think. For example, with the way I'm setting up my JWT parsing middleware right now I have a high cohesion and decoupling. The middleware's client (the code that set's up the application) doesn't worry about the needs of the middleware, the middleware code sets itself up.

But right now I'll be doing more or less what you describe, because middlewares don't work the way I assumed :) To be precise it'll be a factory function creating the current standard middleware factory, and it will be called from the code that does the application setup (in a file called app_setup.py, in my case).
Another thing occurred to me about validity of my approach. I need to exclude some endpoints from the authorization checks. I want implicit inclusion and explicit exclusion in authorization to avaid security issues by omission of including a handler in authorization.

So I don't only need to have a startup code for the middleware - I need it to be able to go through the handlers and decide whether it will wrap them or not. And my case is a bit more complex than looking up a handler function in a set to see if it's excluded:

```python
async def token_reading_middleware(app, handler):
    @wraps(handler)
    async def middleware_handler(request):
        # ...omitted...

    # Getting to the original functions, so we can disregard the wrappers that AioHTTP creates
    # and other potential middlewares on the processing chain.
    handler_original_function = _get_original_function(handler)
    non_secured_original_handlers = {_get_original_function(f)
                                     for f in app.get(NON_SECURED_HANDLERS_KEY, [])}
    if handler_original_function in non_secured_original_handlers:
        return handler
    return middleware_handler

def _get_original_function(function):
    """Assuming that `function` is wrapped in any level of decorators, this will return the original
    function without any decorators.
    """
    while hasattr(function, '__wrapped__'):
        function = function.__wrapped__
    return function
```

True, that can be done during handling of every request, and the slowdown isn't significant compared to all the thing that the framework is doing, but by gradually cutting down these kinds of things we can get a faster framework. And if I'm faced with picking frameworks that have similar functionality, I will pick the faster one.
I suspect you'd be better off using `request.match_info` as is done [here](https://github.com/tutorcruncher/morpheus/blob/b5c9885a83f52cd412a499b69f8de38bcf126cc2/morpheus/app/middleware.py#L11) I know you can get the route name, but without looking I imagine it should also be able to get the coroutine as well if that's what you really need.
@samuelcolvin That's a good idea, I'll do that :) But my general case remains. @asvetlov You would consider my one time factories when I provide a proper implementation?

Resolving patch:
diff --git a/aiohttp/web.py b/aiohttp/web.py
--- a/aiohttp/web.py
+++ b/aiohttp/web.py
@@ -7,6 +7,7 @@
 import warnings
 from argparse import ArgumentParser
 from collections import Iterable, MutableMapping
+from functools import partial
 from importlib import import_module
 
 from yarl import URL
@@ -140,7 +141,7 @@ def freeze(self):
             return
 
         self._frozen = True
-        self._middlewares = tuple(reversed(self._middlewares))
+        self._middlewares = tuple(self._prepare_middleware())
         self._router.freeze()
         self._on_loop_available.freeze()
         self._on_pre_signal.freeze()
@@ -272,6 +273,18 @@ def _make_request(self, message, payload, protocol, writer, task,
             self._loop,
             client_max_size=self._client_max_size)
 
+    def _prepare_middleware(self):
+        for m in reversed(self._middlewares):
+            if getattr(m, '__middleware_version__', None) == 1:
+                # first argument is request or r and this is
+                # new-style middleware
+                yield m, True
+            else:
+                warnings.warn('old-style middleware "{}" deprecated, '
+                              'see #2252'.format(m.__name__),
+                              DeprecationWarning, stacklevel=2)
+                yield m, False
+
     @asyncio.coroutine
     def _handle(self, request):
         match_info = yield from self._router.resolve(request)
@@ -291,8 +304,11 @@ def _handle(self, request):
         if resp is None:
             handler = match_info.handler
             for app in match_info.apps[::-1]:
-                for factory in app._middlewares:
-                    handler = yield from factory(app, handler)
+                for m, new_style in app._middlewares:
+                    if new_style:
+                        handler = partial(m, handler=handler)
+                    else:
+                        handler = yield from m(app, handler)
 
             resp = yield from handler(request)
 
diff --git a/aiohttp/web_middlewares.py b/aiohttp/web_middlewares.py
--- a/aiohttp/web_middlewares.py
+++ b/aiohttp/web_middlewares.py
@@ -6,6 +6,7 @@
 
 
 __all__ = (
+    'middleware',
     'normalize_path_middleware',
 )
 
@@ -23,6 +24,11 @@ def _check_request_resolves(request, path):
     return False, request
 
 
+def middleware(f):
+    f.__middleware_version__ = 1
+    return f
+
+
 def normalize_path_middleware(
         *, append_slash=True, merge_slashes=True,
         redirect_class=HTTPMovedPermanently):
@@ -47,37 +53,32 @@ def normalize_path_middleware(
     """
 
     @asyncio.coroutine
-    def normalize_path_factory(app, handler):
-
-        @asyncio.coroutine
-        def middleware(request):
-
-            if isinstance(request.match_info.route, SystemRoute):
-                paths_to_check = []
-                if '?' in request.raw_path:
-                    path, query = request.raw_path.split('?', 1)
-                    if query:
-                        query = '?' + query
-                else:
-                    query = ''
-                    path = request.raw_path
-
-                if merge_slashes:
-                    paths_to_check.append(re.sub('//+', '/', path))
-                if append_slash and not request.path.endswith('/'):
-                    paths_to_check.append(path + '/')
-                if merge_slashes and append_slash:
-                    paths_to_check.append(
-                        re.sub('//+', '/', path + '/'))
-
-                for path in paths_to_check:
-                    resolves, request = yield from _check_request_resolves(
-                        request, path)
-                    if resolves:
-                        return redirect_class(request.path + query)
-
-            return (yield from handler(request))
-
-        return middleware
-
-    return normalize_path_factory
+    @middleware
+    def normalize_path_middleware(request, handler):
+        if isinstance(request.match_info.route, SystemRoute):
+            paths_to_check = []
+            if '?' in request.raw_path:
+                path, query = request.raw_path.split('?', 1)
+                if query:
+                    query = '?' + query
+            else:
+                query = ''
+                path = request.raw_path
+
+            if merge_slashes:
+                paths_to_check.append(re.sub('//+', '/', path))
+            if append_slash and not request.path.endswith('/'):
+                paths_to_check.append(path + '/')
+            if merge_slashes and append_slash:
+                paths_to_check.append(
+                    re.sub('//+', '/', path + '/'))
+
+            for path in paths_to_check:
+                resolves, request = yield from _check_request_resolves(
+                    request, path)
+                if resolves:
+                    return redirect_class(request.path + query)
+
+        return (yield from handler(request))
+
+    return normalize_path_middleware


Test patch:
diff --git a/tests/test_web_middleware.py b/tests/test_web_middleware.py
--- a/tests/test_web_middleware.py
+++ b/tests/test_web_middleware.py
@@ -7,25 +7,21 @@
 
 @asyncio.coroutine
 def test_middleware_modifies_response(loop, test_client):
-
     @asyncio.coroutine
     def handler(request):
         return web.Response(body=b'OK')
 
     @asyncio.coroutine
-    def middleware_factory(app, handler):
-
-        @asyncio.coroutine
-        def middleware(request):
-            resp = yield from handler(request)
-            assert 200 == resp.status
-            resp.set_status(201)
-            resp.text = resp.text + '[MIDDLEWARE]'
-            return resp
-        return middleware
+    @web.middleware
+    def middleware(request, handler):
+        resp = yield from handler(request)
+        assert 200 == resp.status
+        resp.set_status(201)
+        resp.text = resp.text + '[MIDDLEWARE]'
+        return resp
 
     app = web.Application()
-    app.middlewares.append(middleware_factory)
+    app.middlewares.append(middleware)
     app.router.add_route('GET', '/', handler)
     client = yield from test_client(app)
     resp = yield from client.get('/')
@@ -36,25 +32,20 @@ def middleware(request):
 
 @asyncio.coroutine
 def test_middleware_handles_exception(loop, test_client):
-
     @asyncio.coroutine
     def handler(request):
         raise RuntimeError('Error text')
 
     @asyncio.coroutine
-    def middleware_factory(app, handler):
-
-        @asyncio.coroutine
-        def middleware(request):
-            with pytest.raises(RuntimeError) as ctx:
-                yield from handler(request)
-            return web.Response(status=501,
-                                text=str(ctx.value) + '[MIDDLEWARE]')
-
-        return middleware
+    @web.middleware
+    def middleware(request, handler):
+        with pytest.raises(RuntimeError) as ctx:
+            yield from handler(request)
+        return web.Response(status=501,
+                            text=str(ctx.value) + '[MIDDLEWARE]')
 
     app = web.Application()
-    app.middlewares.append(middleware_factory)
+    app.middlewares.append(middleware)
     app.router.add_route('GET', '/', handler)
     client = yield from test_client(app)
     resp = yield from client.get('/')
@@ -65,27 +56,22 @@ def middleware(request):
 
 @asyncio.coroutine
 def test_middleware_chain(loop, test_client):
-
     @asyncio.coroutine
     def handler(request):
         return web.Response(text='OK')
 
-    def make_factory(num):
-
+    def make_middleware(num):
         @asyncio.coroutine
-        def factory(app, handler):
-
-            def middleware(request):
-                resp = yield from handler(request)
-                resp.text = resp.text + '[{}]'.format(num)
-                return resp
-
-            return middleware
-        return factory
+        @web.middleware
+        def middleware(request, handler):
+            resp = yield from handler(request)
+            resp.text = resp.text + '[{}]'.format(num)
+            return resp
+        return middleware
 
     app = web.Application()
-    app.middlewares.append(make_factory(1))
-    app.middlewares.append(make_factory(2))
+    app.middlewares.append(make_middleware(1))
+    app.middlewares.append(make_middleware(2))
     app.router.add_route('GET', '/', handler)
     client = yield from test_client(app)
     resp = yield from client.get('/')
@@ -217,3 +203,90 @@ def test_append_and_merge_slash(self, path, status, cli):
         client = yield from cli(extra_middlewares)
         resp = yield from client.get(path)
         assert resp.status == status
+
+
+@asyncio.coroutine
+def test_old_style_middleware(loop, test_client):
+    @asyncio.coroutine
+    def handler(request):
+        return web.Response(body=b'OK')
+
+    @asyncio.coroutine
+    def middleware_factory(app, handler):
+
+        @asyncio.coroutine
+        def middleware(request):
+            resp = yield from handler(request)
+            assert 200 == resp.status
+            resp.set_status(201)
+            resp.text = resp.text + '[old style middleware]'
+            return resp
+        return middleware
+
+    with pytest.warns(DeprecationWarning) as warning_checker:
+        app = web.Application()
+        app.middlewares.append(middleware_factory)
+        app.router.add_route('GET', '/', handler)
+        client = yield from test_client(app)
+        resp = yield from client.get('/')
+        assert 201 == resp.status
+        txt = yield from resp.text()
+        assert 'OK[old style middleware]' == txt
+
+    assert len(warning_checker) == 1
+    msg = str(warning_checker.list[0].message)
+    assert 'old-style middleware "middleware_factory" deprecated' in msg
+
+
+@asyncio.coroutine
+def test_mixed_middleware(loop, test_client):
+    @asyncio.coroutine
+    def handler(request):
+        return web.Response(body=b'OK')
+
+    @asyncio.coroutine
+    def m_old1(app, handler):
+        @asyncio.coroutine
+        def middleware(request):
+            resp = yield from handler(request)
+            resp.text += '[old style 1]'
+            return resp
+        return middleware
+
+    @asyncio.coroutine
+    @web.middleware
+    def m_new1(request, handler):
+        resp = yield from handler(request)
+        resp.text += '[new style 1]'
+        return resp
+
+    @asyncio.coroutine
+    def m_old2(app, handler):
+        @asyncio.coroutine
+        def middleware(request):
+            resp = yield from handler(request)
+            resp.text += '[old style 2]'
+            return resp
+        return middleware
+
+    @asyncio.coroutine
+    @web.middleware
+    def m_new2(request, handler):
+        resp = yield from handler(request)
+        resp.text += '[new style 2]'
+        return resp
+
+    middlewares = m_old1, m_new1, m_old2, m_new2
+
+    with pytest.warns(DeprecationWarning) as w:
+        app = web.Application(middlewares=middlewares)
+        app.router.add_route('GET', '/', handler)
+        client = yield from test_client(app)
+        resp = yield from client.get('/')
+        assert 200 == resp.status
+        txt = yield from resp.text()
+        assert 'OK[new style 2][old style 2][new style 1][old style 1]' == txt
+
+    assert len(w) == 2
+    assert 'old-style middleware "m_old2" deprecated' in str(w.list[0].message)
+    assert 'old-style middleware "m_old1" deprecated' in str(w.list[1].message)