Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signals http.middleware.before and http.middleware.after are dispatched for _every_ middleware #2352

Open
ashleysommer opened this issue Jan 4, 2022 · 9 comments
Labels

Comments

@ashleysommer
Copy link
Member

ashleysommer commented Jan 4, 2022

For a single request, the signals http.middleware.before and http.middleware.after are dispatched for every middle on the app.

Example app:

from sanic import Sanic
from sanic.response import html

app = Sanic(__name__)

@app.middleware("request")
def my_middleware1(request):
    print("Middleware 1 ran", flush=True)

@app.middleware("request")
def my_middleware2(request):
    print("Middleware 2 ran", flush=True)

@app.signal("http.middleware.before", condition={"attach_to": "request"})
def handle_mw_before(request, response=None):
    print("Before middleware", flush=True)

@app.signal("http.middleware.after", condition={"attach_to": "request"})
def handle_mw_after(request, response):
    print("After middleware", flush=True)

@app.get("/")
def index(request):
    return html("<p>hello world</p>")

if __name__ == "__main__":
    app.run(debug=True, auto_reload=False, access_log=False)

Output:

[2022-01-04 13:57:50 +1000] [47672] [INFO] Starting worker [47672]
[2022-01-04 13:57:54 +1000] [47672] [DEBUG] Dispatching signal: http.middleware.before
Before middleware
Middleware 1 ran
[2022-01-04 13:57:54 +1000] [47672] [DEBUG] Dispatching signal: http.middleware.after
After middleware
[2022-01-04 13:57:54 +1000] [47672] [DEBUG] Dispatching signal: http.middleware.before
Before middleware    #<--- Run again
Middleware 2 ran
[2022-01-04 13:57:54 +1000] [47672] [DEBUG] Dispatching signal: http.middleware.after
After middleware     #<--- Run again

Applicable section of code:

sanic/sanic/app.py

Lines 1346 to 1372 in a7bc8b5

for middleware in applicable_middleware:
await self.dispatch(
"http.middleware.before",
inline=True,
context={
"request": request,
"response": None,
},
condition={"attach_to": "request"},
)
response = middleware(request)
if isawaitable(response):
response = await response
await self.dispatch(
"http.middleware.after",
inline=True,
context={
"request": request,
"response": None,
},
condition={"attach_to": "request"},
)
if response:
return response

It appears this is done intentionally. I don't know what the usefulness of running a handler for every middleware is, except for perhaps debug tracing or handler logger.

Maybe it makes sense to create two new signals like http.middleware.before_all and http.middleware.after_all, in a place like this:

await self.dispatch( 
    "http.middleware.before_all", 
    inline=True, 
    context={ 
        "request": request, 
        "response": None, 
    }, 
    condition={"attach_to": "request"}, 
)
response = None
if applicable_middleware and not request.request_middleware_started:
    request.request_middleware_started = True
    for middleware in applicable_middleware: 
         await self.dispatch( 
             "http.middleware.before", 
             inline=True, 
             context={ 
                 "request": request, 
                 "response": None, 
             }, 
             condition={"attach_to": "request"}, 
         ) 
      
         response = middleware(request) 
         if isawaitable(response): 
             response = await response 
      
         await self.dispatch( 
             "http.middleware.after", 
             inline=True, 
             context={ 
                 "request": request, 
                 "response": None, 
             }, 
             condition={"attach_to": "request"}, 
         ) 
      
         if response: 
             break
await self.dispatch( 
    "http.middleware.after_all", 
    inline=True, 
    context={ 
        "request": request, 
        "response": response, 
    }, 
    condition={"attach_to": "request"}, 
)
return response
@ahopkins
Copy link
Member

ahopkins commented Jan 4, 2022

Correct, that is intentional. Out of curiosity, what is the use case that you are after with dispatching once before and after middleware? Is it to have control over when something executes? There are certainly other signals you could potentially use if you need to do something earlier, like when the request is created, or routing is performed. Will these help? Or, are you particularly interested in creating a sort of "priority" middleware?

The reason I ask is that there is a need for a bit of a revamp in middleware this year anyway. We have been talking about this for a long time now, and I think with some of the other improvements we made last year, we are finally getting close to being there. There are two main changes I think we should make:

  1. precompute middleware and merge it into the handler itself
  2. add an API for prioritizing middleware and not relying upon definition ordering

@ashleysommer
Copy link
Member Author

ashleysommer commented Jan 4, 2022

I am trying to emulate the capabilities of Sanic-Plugin-Toolkit without monkey-patching, using the signals mechanism.

I need to be able to run a queue of registered plugin middlewares before the application's middlewares are run.

Putting it on "http.lifecycle.handle" does not make sense because the router hasn't run yet.

The most logical place to put it would be on "http.routing.after", but at that stage recv_body() hasn't been called yet, so requests have no body.

Given it is specific to a middleware use-case, that is why I proposed a signal called "http.middleware.before_all".

This leads into the other conversation in #2353 where I need to be able to check if any of those plugin middlewares produced an early response, and that needs to be passed back from the signal handler, through the signal dispatcher, to the calling site. That is not possible so I think this needs to wait until a revamp of the middleware stack.

@ahopkins
Copy link
Member

ahopkins commented Jan 4, 2022

Actually, I think better than http.middleware.before_all would be http.lifecycle.body that executes here after reading the body.

image

@ashleysommer
Copy link
Member Author

Yeah, I thought an alternative would be after the receive_body() call.

I think middleware.after_all could be provided by http.lifecycle.respond.

@ahopkins
Copy link
Member

ahopkins commented Jan 4, 2022

We should probably also include a lifecycle diagram in the docs with the flow of handlers, middleware, and signals.

@stale
Copy link

stale bot commented Apr 16, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If this is incorrect, please respond with an update. Thank you for your contributions.

@stale stale bot added the stale label Apr 16, 2022
@ahopkins ahopkins removed the stale label Apr 17, 2022
@ChihweiLHBird
Copy link
Member

ChihweiLHBird commented May 11, 2022

Actually, I think better than http.middleware.before_all would be http.lifecycle.body that executes here after reading the body.

image

I think (tested) current version of Sanic doesn't read the body if the request type is GET without any payload in the body and hence won't trigger the event http.lifecycle.read_body...

Like

app = Sanic("MyHelloWorldApp")


@app.get("/")
async def hello_world1(request):
    return text("Hello, world.")

@app.signal("http.lifecycle.read_body")
async def http_lifecycle_read_body(body):
    print("http.lifecycle.read_body")

app.run()

@ChihweiLHBird
Copy link
Member

ChihweiLHBird commented May 11, 2022

Never mind, because http.lifecycle.read_body is triggered inside read function, and we can still add another trigger for http.lifecycle.body somewhere after the code that is reading the body, even in the case that the body wasn't really read, it will still trigger the signal. But it remains a little bit weird to call the signal body.

@stale
Copy link

stale bot commented Sep 21, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If this is incorrect, please respond with an update. Thank you for your contributions.

@stale stale bot added the stale label Sep 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants