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

Consider a request/response wrapping hook slightly higher level than asgi_wrapper() #2168

Open
simonw opened this issue Aug 31, 2023 · 6 comments

Comments

@simonw
Copy link
Owner

simonw commented Aug 31, 2023

There's a long justification for why this might be needed here:

Short version: it would be neat if it was possible to stash some data on the request object such that a later plugin/middleware-type-thing could use that to influence the final returned response - similar to the kinds of things you can do with Django middleware.

The asgi_wrapper() mechanism doesn't have access to the request or response objects - it gets scope and can mess around with receive and send, but those are pretty low-level primitives.

Since Datasette has well-defined request and response objects now it might be nice to have a middleware layer that can manipulate those directly.

@simonw
Copy link
Owner Author

simonw commented Aug 31, 2023

Not sure what to call this. Maybe app_wrapper()?

Or perhaps it's simpler than that, something like this:

@hookspec
def process_response(datasette, request, response):
    """Last chance to modify the response before it is returned to the client"""

@simonw
Copy link
Owner Author

simonw commented Aug 31, 2023

This could even be a pair of hooks - process_request() and process_response().

Or could take a leaf from Django, which redesigned middleware to use this pattern instead:

def simple_middleware(get_response):
    # One-time configuration and initialization.
    def middleware(request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.
        response = get_response(request)
        # Code to be executed for each request/response after
        # the view is called.
        return response
    return middleware

Or even borrow an idea from pytest where fixtures can yield in the middle, like this:

@pytest.fixture
def sending_user(mail_admin):
    user = mail_admin.create_user()
    yield user
    mail_admin.delete_user(user)

@simonw
Copy link
Owner Author

simonw commented Aug 31, 2023

A pattern like this could be interesting:

async def my_middleware(datasette, request, get_response):
    # Mess with request here if neccessary
    response = await get_response(request)
    # mess with response
    return response

The Django pattern is more complicated but does have that mechanism for running one-time configuration prior to defining the middleware() function, which is neat.

@simonw
Copy link
Owner Author

simonw commented Aug 31, 2023

The hook could be called register_middleware() and could work like register_routes() and register_commands():

@hookspec
def register_middleware(datasette):
    """Register middleware: returns a list of async def middleware functions"""

@simonw
Copy link
Owner Author

simonw commented Aug 31, 2023

Need to make sure the design of this takes streaming responses into account. Those could be pretty tricky here.

I nice thing about asgi_wrapper() is that it handles those already.

@simonw
Copy link
Owner Author

simonw commented Sep 10, 2023

This looks relevant:

https://pluggy.readthedocs.io/en/stable/#wrappers

A hookimpl can be marked with the "wrapper" option, which indicates that the function will be called to wrap (or surround) all other normal hookimpl calls. A hook wrapper can thus execute some code ahead and after the execution of all corresponding non-wrappper hookimpls.

This could be the perfect mechanism for implementing this hook, although I still need to figure out how it interacts with streaming.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant