Repository: sanic-org/sanic

Issue:
Blueprint middleware applied globally
I was a little too hasty updating the blueprints documentation for middleware/exceptions, and just realized that middlewares and exceptions registered through a blueprint decorator are applied to all routes.

Is this intended behaviour? If so, then the the blueprint documentation must be updated once more.

Alternative behaviour: middleware registered on a blueprint, are only applied to routes for that blueprint.
Some considerations:
- should middleware applied to the app-object (instance of Sanic) also be applied to blueprint middleware?
- if so, how will ordering be handled?



Discussion:
Ah, I missed that in the pull request.  I've updated the documentation for now.  My thoughts on this are:

Applied locally:
- Is more self-contained, prevents conflicting code.
- Applying global middleware and exception handling requires registering them separately from the blueprint.

Applied globally:
- Modularizes middleware and exception handling.
- Blueprints can facilitate whole modules, allowing a single interface for sharing code between projects.
- Can use decorators to achieve local middleware and exception handling:

``` python
def authenticated(func):
    def handler(request, *args, **kwargs):
        # Middleware goes here
        try:
            return func(request, *args, **kwargs)
        except MyCustomException:
            return text("son, i am dissapoint")
    return handler

@bp.route('/')
@authenticated
def index(request):
    return text("We did it fam")

...
```

In both scenarios you can achieve the same thing, but I think the latter offers more.

As for ordering, I'm thinking they should be applied in the order the blueprint was registered, and in the order they were registered in the blueprint.  I think it would allow blueprints to require other blueprints in a way that's easy to discern in the code.

My concern with using a decorator for local middleware/exceptions, is that it requires a different way of adding them, compared to registering on the application object. My fear is, that it becomes too complicated or too involved to handle middleware/exceptions on blueprints.

I would prefer, that you register middleware and exceptions in the same manner (`@<obj>.middleware`), regardless of applying it on the app-object (affecting all routes) or the blueprint (affecting only those blueprint routes).

What is the different between using `@app.middleware` and `@bp.middleware`? Are there any benefits to use `@bp.middleware`?
To add my two cents: While it might be beneficial to apply middleware registered on a blueprint globally, that's definitely not what I'd expect to happen. I'd regard blueprints as pretty self-contained and separate units and find it kinda surprising to see them interact in such a way. What about adding a `@bp.global_middleware`?
It's defnly unexpected behavior. Well, for me with a long experience with Flask that has same concept the 'Blueprint', it surprised me a lot! I think that the blueprint is a modular sub-*application*. Unless it stands just for deleting all url prefixes in code, an application with blueprints would be like an 'united' application, not 'single' one. I can understand complexity fears but I believe that it can offer much more capability and convenience with blueprint's independence.

> @eth3lbert) What is the different between using @app.middleware and @bp.middleware? Are there any benefits to use @bp.middleware?

@eth3lbert : You can apply various caching policy, control access to specific spaces, or inject something into your response only when it was processed in some bp, based on your blueprint separation.

> @channelcat) As for ordering, I'm thinking they should be applied in the order the blueprint was registered, and in the order they were registered in the blueprint.

👍 
I also think this is a counterintuitive behavior. Especially when you introduce this project as flask-like, I expected the blueprint-related features are totally separated and it doesn't affect the app out of blueprint.

I agree about the concolusion the global application offers more, still I also more agree to the other parts its name must not be `Blueprint.middleware`. Strongly voting to @Tim-Erwin's idea, you don't need to select only one idea of middleware behavior on the counterintuitive name. Please consider to let Blueprint has its own scoped middleware and allow them also to have modulized global middlewares like `Blueprint.app_middleware` or `Blueprint.global_middleware`.
Is this still an issue? Will reopen if necessary.
Reopening per request from @r0fls 
Is it still there? any update on this?
still there
Does this still apply because of #715 being fixed?
Example from life: I have an application with admin control. In `admin` blueprint I have to use decorator for check admin rights for all views instead of one blueprint middleware handler.
And do not forget about this please

> Explicit is better than implicit.
Another real-world example. 
Two URL patterns:
```
   /a/b/<cred1>/<cred2>
   /a/b?cred1=abc&cred2=xyz

```
Planned to have two middlewares that gathered the necessary parameters (one from the URL, another from arguments) and store them in the request object so the next layer of code would not need to worry about where the creds 'came from.'
(There's actually more than two, and some are significantly different from the examples.)
Personally I would expect `@app.middleware` to apply globally and `@bp.middleware` to apply locally.

Another real life example (which led me here):

```python
public = Blueprint("public", url_prefix="/public")
public.static("/", public_web_dir)

secure = Blueprint("secure", url_prefix="/secure")
secure.static("/", secure_web_dir)


@secure.middleware("request")
@authenticated
async def request_middleware(request):
    pass


app = Sanic()
app.blueprint(public)
app.blueprint(secure)
app.run(host="0.0.0.0", port=8080, debug=True)
```

In this case I expect to be able to access the static files served via `public` and be stopped from accessing the static files served via `secure` without authorization.

Due to the nature of `bp.static` I cannot add my decorator to the requests it manages, which makes using `.middleware` for all requests going to `secure` the logical solution. However, with the current globalness of `.middleware` I will need to come up with a more complicated solution.

---

While I definitely agree with @Tim-Erwin that it would be more logical to have a `.global_middleware` for the current behavior of `.middleware` for blueprints, maybe the introduction of `.local_middleware` could be an option to maintain compatibility?
I have a workaround (although I still think this should be implemented in sanic). Using my last example:

```python
public = Blueprint("public", url_prefix="/public")
public.static("/", public_web_dir)

secure = Blueprint("secure", url_prefix="/secure")
secure.static("/", secure_web_dir)


@secure.middleware("request")
def global_middleware(request):
    if request.path.startswith(secure.url_prefix):

        @authenticated
        def local_middleware(request):
            pass
        local_middleware(request)


app = Sanic()
app.blueprint(public)
app.blueprint(secure)
app.run(host="0.0.0.0", port=8080, debug=True)
```
I just wanted to agree that my expectation for a blueprint middleware is that it applies only to routes included in the blueprint.

I don't see why it would apply globally. My sample handler setup which seems to do what I would like:

```
from sanic import response
from sanic import Blueprint
from tempfile import TemporaryFile

blueprint = Blueprint('docx_server', url_prefix='/docx')

@blueprint.middleware('request')
async def type_conversion_dates(request):
  request.args['date'] = pendulum.now()

@blueprint.middleware('response')
async def set_model_response(request, response):
  response.content_type ='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
  return response

@blueprint.route("/<template>", methods=['POST'], strict_slashes=False)
async def get_template_docx(request, template):
  """Returns X3D document
    context (json):
      employee_name (str): name
      employee_title (str): title
      employee_education (str[]): education
      employee_certification (str[]): certifications
      selected_experience (str): experience description
      extra_experience (ob[{title, description}]):
        title: of description
        description: experience description
      employee_organizations(str[]): organizations

    Args:
      template (str): render template name

    Returns:
      docx
  """
  async with request.app.db_pool.acquire() as con:
    template_name = request.args.get('template')
    doc = DocxTemplate(f'/app/templates/{template_name}')
    context = request.json
    doc.render(context)
    tf = TemporaryFile()
    doc.save(tf)

    dt_string = dt.format('YYYY-MM-DD')
    r.headers['Content-Disposition'] = f'filename="{dt_string}-{context.employee_name}-resume.docx"'
    return await response.file(tf)
```

This seems self contained and easy to manage. Except, now I have to add the middleware somewhere else, disconnecting it from an endpoint I expect to only take certain requests and only serve certain responses.
Hi! I see there's no update for pass 3 years. Is there any plan to fix or somehow differently resolve this issue? The current behavior seems rather counterintuitive.
I believe that this should be fixed (make them local). However, currently routing is performed *after* request middlewares, so it would be a rather big and potentially breaking architectural change. In particular, one would need to consider whether existing middlewares rely on the current behaviour, e.g. to change request url or method prior to routing.
I believe in that in general (among other languages) middleware should be applied after the routing and is what many people expect the middleware to do. You are right with that it is breaking change so I am not sure what the optimal solution would be. Maybe as @FelixZY suggested add `local_middleware`? But if this should be implemented, it would probably make more sense to make `middleware` function local by default.
I just wanted to pop into this thread to point out that the [current documentation for blueprint group middleware](https://sanic.readthedocs.io/en/latest/sanic/blueprints.html#blueprint-group-middleware) implies (incorrectly) that blueprint group middleware only executes on the routes defined in that blueprint group (and not globally).

> Using this middleware will ensure that you can apply a common middleware to all the blueprints that form the current blueprint group under consideration.

The example code also makes it appear that middleware applied to a blueprint only impacts routes under that blueprint. Specifically this code from the example:

```
@bp1.middleware('request')
async def bp1_only_middleware(request):
    print('applied on Blueprint : bp1 Only')
```


I'm wondering where this issue stands. I am using 19.6.0 (but tried 19.6.3) and I am seeing my blueprint middleware being applied globally. I tried wrapping it up and a blueprint group as well but the behavior is the same.

My understanding of the latest documentation is that middleware added to a blueprint will be global unless the blueprint is added to a blueprint group. In this case it should apply only to routes in that group.

While this distinction is a bit confusing, I can live with it but it doesn't seem to be working that way.

Any help would be appreciated.
Let me take a look. If it's an easy enough fix without major refactoring, let me see if I can open a quick PR to address this. 
@huge-success/sanic-core-devs This is getting a bit interesting. I was able to get the blueprint based middleware to work the right way without changing much, But here is a curious case, 

```python
def test_bp_middleware(app):
    blueprint = Blueprint("test_middleware")

    @blueprint.middleware("response")
    async def process_response(request, response):
        return text("OK")

    @app.route("/")
    async def handler(request):
        return text("FAIL")

    app.blueprint(blueprint)

    request, response = app.test_client.get("/")

    assert response.status == 200
    assert response.text == "OK"
```

This is from one of the existing test cases. Now, if we make sure that the blueprint middleware gets applied only on the route registered with the middleware, then this expected output is invalid. Since the route was created using `@app` and not `@blueprint`.

What would be the best behavior in this case?

1. If you register a middleware via `@blueprint.middleware` then it will apply only to the routes defined by the blueprint.
2. If you register a middleware via `@blueprint_group.middleware` then it will apply to all blueprint based routes that are part of the group. 
3. If you define a middleware via `@app.middleware` then it will be applied on all available routes

With the above in mind, what is the expected precedence in which this can be applied ?

Resolving patch:
diff --git a/sanic/app.py b/sanic/app.py
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -79,7 +79,8 @@ def __init__(
         self.is_request_stream = False
         self.websocket_enabled = False
         self.websocket_tasks = set()
-
+        self.named_request_middleware = {}
+        self.named_response_middleware = {}
         # Register alternative method names
         self.go_fast = self.run
 
@@ -172,7 +173,7 @@ def route(
         :param stream:
         :param version:
         :param name: user defined route name for url_for
-        :return: decorated function
+        :return: tuple of routes, decorated function
         """
 
         # Fix case where the user did not prefix the URL with a /
@@ -198,7 +199,7 @@ def response(handler):
             if stream:
                 handler.is_stream = stream
 
-            self.router.add(
+            routes = self.router.add(
                 uri=uri,
                 methods=methods,
                 handler=handler,
@@ -207,7 +208,7 @@ def response(handler):
                 version=version,
                 name=name,
             )
-            return handler
+            return routes, handler
 
         return response
 
@@ -456,7 +457,7 @@ def websocket(
         :param subprotocols: optional list of str with supported subprotocols
         :param name: A unique name assigned to the URL so that it can
                      be used with :func:`url_for`
-        :return: decorated function
+        :return: tuple of routes, decorated function
         """
         self.enable_websocket()
 
@@ -509,7 +510,7 @@ async def websocket_handler(request, *args, **kwargs):
                     self.websocket_tasks.remove(fut)
                 await ws.close()
 
-            self.router.add(
+            routes = self.router.add(
                 uri=uri,
                 handler=websocket_handler,
                 methods=frozenset({"GET"}),
@@ -517,7 +518,7 @@ async def websocket_handler(request, *args, **kwargs):
                 strict_slashes=strict_slashes,
                 name=name,
             )
-            return handler
+            return routes, handler
 
         return response
 
@@ -538,6 +539,7 @@ def add_websocket_route(
         :param host: Host IP or FQDN details
         :param uri: URL path that will be mapped to the websocket
                     handler
+                    handler
         :param strict_slashes: If the API endpoint needs to terminate
                 with a "/" or not
         :param subprotocols: Subprotocols to be used with websocket
@@ -639,6 +641,22 @@ def register_middleware(self, middleware, attach_to="request"):
                 self.response_middleware.appendleft(middleware)
         return middleware
 
+    def register_named_middleware(
+        self, middleware, route_names, attach_to="request"
+    ):
+        if attach_to == "request":
+            for _rn in route_names:
+                if _rn not in self.named_request_middleware:
+                    self.named_request_middleware[_rn] = deque()
+                if middleware not in self.named_request_middleware[_rn]:
+                    self.named_request_middleware[_rn].append(middleware)
+        if attach_to == "response":
+            for _rn in route_names:
+                if _rn not in self.named_response_middleware:
+                    self.named_response_middleware[_rn] = deque()
+                if middleware not in self.named_response_middleware[_rn]:
+                    self.named_response_middleware[_rn].append(middleware)
+
     # Decorator
     def middleware(self, middleware_or_request):
         """
@@ -910,20 +928,23 @@ async def handle_request(self, request, write_callback, stream_callback):
         # allocation before assignment below.
         response = None
         cancelled = False
+        name = None
         try:
+            # Fetch handler from router
+            handler, args, kwargs, uri, name = self.router.get(request)
+
             # -------------------------------------------- #
             # Request Middleware
             # -------------------------------------------- #
-            response = await self._run_request_middleware(request)
+            response = await self._run_request_middleware(
+                request, request_name=name
+            )
             # No middleware results
             if not response:
                 # -------------------------------------------- #
                 # Execute Handler
                 # -------------------------------------------- #
 
-                # Fetch handler from router
-                handler, args, kwargs, uri = self.router.get(request)
-
                 request.uri_template = uri
                 if handler is None:
                     raise ServerError(
@@ -987,7 +1008,7 @@ async def handle_request(self, request, write_callback, stream_callback):
             if response is not None:
                 try:
                     response = await self._run_response_middleware(
-                        request, response
+                        request, response, request_name=name
                     )
                 except CancelledError:
                     # Response middleware can timeout too, as above.
@@ -1259,10 +1280,14 @@ async def trigger_events(self, events, loop):
             if isawaitable(result):
                 await result
 
-    async def _run_request_middleware(self, request):
+    async def _run_request_middleware(self, request, request_name=None):
         # The if improves speed.  I don't know why
-        if self.request_middleware:
-            for middleware in self.request_middleware:
+        named_middleware = self.named_request_middleware.get(
+            request_name, deque()
+        )
+        applicable_middleware = self.request_middleware + named_middleware
+        if applicable_middleware:
+            for middleware in applicable_middleware:
                 response = middleware(request)
                 if isawaitable(response):
                     response = await response
@@ -1270,9 +1295,15 @@ async def _run_request_middleware(self, request):
                     return response
         return None
 
-    async def _run_response_middleware(self, request, response):
-        if self.response_middleware:
-            for middleware in self.response_middleware:
+    async def _run_response_middleware(
+        self, request, response, request_name=None
+    ):
+        named_middleware = self.named_response_middleware.get(
+            request_name, deque()
+        )
+        applicable_middleware = self.response_middleware + named_middleware
+        if applicable_middleware:
+            for middleware in applicable_middleware:
                 _response = middleware(request, response)
                 if isawaitable(_response):
                     _response = await _response
diff --git a/sanic/blueprints.py b/sanic/blueprints.py
--- a/sanic/blueprints.py
+++ b/sanic/blueprints.py
@@ -104,6 +104,8 @@ def register(self, app, options):
 
         url_prefix = options.get("url_prefix", self.url_prefix)
 
+        routes = []
+
         # Routes
         for future in self.routes:
             # attach the blueprint name to the handler so that it can be
@@ -114,7 +116,7 @@ def register(self, app, options):
 
             version = future.version or self.version
 
-            app.route(
+            _routes, _ = app.route(
                 uri=uri[1:] if uri.startswith("//") else uri,
                 methods=future.methods,
                 host=future.host or self.host,
@@ -123,6 +125,8 @@ def register(self, app, options):
                 version=version,
                 name=future.name,
             )(future.handler)
+            if _routes:
+                routes += _routes
 
         for future in self.websocket_routes:
             # attach the blueprint name to the handler so that it can be
@@ -130,21 +134,27 @@ def register(self, app, options):
             future.handler.__blueprintname__ = self.name
             # Prepend the blueprint URI prefix if available
             uri = url_prefix + future.uri if url_prefix else future.uri
-            app.websocket(
+            _routes, _ = app.websocket(
                 uri=uri,
                 host=future.host or self.host,
                 strict_slashes=future.strict_slashes,
                 name=future.name,
             )(future.handler)
+            if _routes:
+                routes += _routes
 
+        route_names = [route.name for route in routes]
         # Middleware
         for future in self.middlewares:
             if future.args or future.kwargs:
-                app.register_middleware(
-                    future.middleware, *future.args, **future.kwargs
+                app.register_named_middleware(
+                    future.middleware,
+                    route_names,
+                    *future.args,
+                    **future.kwargs
                 )
             else:
-                app.register_middleware(future.middleware)
+                app.register_named_middleware(future.middleware, route_names)
 
         # Exceptions
         for future in self.exceptions:
diff --git a/sanic/router.py b/sanic/router.py
--- a/sanic/router.py
+++ b/sanic/router.py
@@ -140,21 +140,22 @@ def add(
             docs for further details.
         :return: Nothing
         """
+        routes = []
         if version is not None:
             version = re.escape(str(version).strip("/").lstrip("v"))
             uri = "/".join(["/v{}".format(version), uri.lstrip("/")])
         # add regular version
-        self._add(uri, methods, handler, host, name)
+        routes.append(self._add(uri, methods, handler, host, name))
 
         if strict_slashes:
-            return
+            return routes
 
         if not isinstance(host, str) and host is not None:
             # we have gotten back to the top of the recursion tree where the
             # host was originally a list. By now, we've processed the strict
             # slashes logic on the leaf nodes (the individual host strings in
             # the list of host)
-            return
+            return routes
 
         # Add versions with and without trailing /
         slashed_methods = self.routes_all.get(uri + "/", frozenset({}))
@@ -176,10 +177,12 @@ def add(
         )
         # add version with trailing slash
         if slash_is_missing:
-            self._add(uri + "/", methods, handler, host, name)
+            routes.append(self._add(uri + "/", methods, handler, host, name))
         # add version without trailing slash
         elif without_slash_is_missing:
-            self._add(uri[:-1], methods, handler, host, name)
+            routes.append(self._add(uri[:-1], methods, handler, host, name))
+
+        return routes
 
     def _add(self, uri, methods, handler, host=None, name=None):
         """Add a handler to the route list
@@ -328,6 +331,7 @@ def merge_route(route, methods, handler):
             self.routes_dynamic[url_hash(uri)].append(route)
         else:
             self.routes_static[uri] = route
+        return route
 
     @staticmethod
     def check_dynamic_route_exists(pattern, routes_to_check, parameters):
@@ -442,6 +446,7 @@ def _get(self, url, method, host):
             method=method,
             allowed_methods=self.get_supported_methods(url),
         )
+
         if route:
             if route.methods and method not in route.methods:
                 raise method_not_supported
@@ -476,7 +481,7 @@ def _get(self, url, method, host):
         route_handler = route.handler
         if hasattr(route_handler, "handlers"):
             route_handler = route_handler.handlers[method]
-        return route_handler, [], kwargs, route.uri
+        return route_handler, [], kwargs, route.uri, route.name
 
     def is_stream_handler(self, request):
         """ Handler for request is stream or not.


Test patch:
diff --git a/tests/test_app.py b/tests/test_app.py
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -94,7 +94,7 @@ async def handler():
 
 def test_app_handle_request_handler_is_none(app, monkeypatch):
     def mockreturn(*args, **kwargs):
-        return None, [], {}, ""
+        return None, [], {}, "", ""
 
     # Not sure how to make app.router.get() return None, so use mock here.
     monkeypatch.setattr(app.router, "get", mockreturn)
diff --git a/tests/test_blueprint_group.py b/tests/test_blueprint_group.py
--- a/tests/test_blueprint_group.py
+++ b/tests/test_blueprint_group.py
@@ -83,7 +83,7 @@ def enhance_response_middleware(request: Request, response: HTTPResponse):
     _, response = app.test_client.patch("/api/bp2/route/bp2", headers=header)
     assert response.text == "PATCH_bp2"
 
-    _, response = app.test_client.get("/v2/api/bp1/request_path")
+    _, response = app.test_client.put("/v2/api/bp1/request_path")
     assert response.status == 401
 
 
@@ -141,8 +141,8 @@ def app_default_route(request):
     _, response = app.test_client.get("/api/bp3")
     assert response.text == "BP3_OK"
 
-    assert MIDDLEWARE_INVOKE_COUNTER["response"] == 4
-    assert MIDDLEWARE_INVOKE_COUNTER["request"] == 4
+    assert MIDDLEWARE_INVOKE_COUNTER["response"] == 3
+    assert MIDDLEWARE_INVOKE_COUNTER["request"] == 2
 
 
 def test_bp_group_list_operations(app: Sanic):
diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py
--- a/tests/test_blueprints.py
+++ b/tests/test_blueprints.py
@@ -268,7 +268,7 @@ async def handler(request):
     request, response = app.test_client.get("/")
 
     assert response.status == 200
-    assert response.text == "OK"
+    assert response.text == "FAIL"
 
 
 def test_bp_exception_handler(app):