evansd / whitenoise

Radically simplified static file serving for Python web apps
https://whitenoise.readthedocs.io
MIT License
2.51k stars 148 forks source link

Running whitenoise behind a WSGI-to-ASGI adapter #251

Open aaronn opened 4 years ago

aaronn commented 4 years ago

I'm about to deploy a django app. I was originally going to use uvicorn with Django 3.0, but after seeing whitenoise doesn't work with asgi servers yet was thinking of just reverting back to wsgi.

I just came across @tomchristie's pull request and he mentions he's able to (non-ideally) get it working with a wsgi-to-asgi adapter. I was wondering if there's any additional information on how to get that working.

Ideally I'd like to just swap that out whenever whitenoise goes asgi, instead of reverting from uvicorn to gunicorn.

Thanks for the continued work on a great project!

evansd commented 4 years ago

Hi @aaronn, it's been one of my long term goals get whitenoise working natively wth asgi, but I just haven't had time to make any progress on this. It should certainly be easier now that I've dropped support for older Python versions and there's no issue with using the async keywords. But right now I don't have any specific advice on getting this working I'm afraid.

thenewguy commented 4 years ago

@evansd @andrewgodwin

Does Whitenoise work with Django's Daphne server? I see various reference to using Whitenoise but am confused because Daphne is ASGI yet seems to recommend Whitenoise. However, Whitenoise indicates it doesn't work with ASGI.

It would be nice to move to Daphne instead of Gunicorn to make use of websockets but compatibility is contradictory.

andrewgodwin commented 4 years ago

It's possible I was mistaken, but I used to use Whitenoise in Django under ASGI since it interacted with the Django request interface, which we provide a synchronous emulation of. Not sure if it still works, but unless it somehow pierces though Django to get a WSGI handle, it should behave fine behind the synchronous emulation layer.

Of course, that requires Django; you can't use it directly with ASGI since it's a different interface. There's asgiref.wsgi which provides a basic WSGI-to-ASGI adapter that it might work with, though.

thenewguy commented 4 years ago

@andrewgodwin Makes sense!

@evansd Is it correct that Whitenoise is compatible with Django served by ASGI if using middleware integration? Just barebones ASGI integration is unsupported?

evansd commented 4 years ago

Hi, yes DjangoWhitenoise acts as standard Django middleware so should run fine via the compatibility layer. I'd love to get native ASGI integration into Whitenoise but super busy with the medical day job at the moment and ENOTIME :)

kmichel2 commented 4 years ago

Hi, I've had success in using WhiteNoiseMiddleware in an async context in Django, but I noticed that WhiteNoiseMiddleware does not inherit from MiddlewareMixin.

The consequence is that WhiteNoiseMiddleware does not get the full async compatibility layer from Django 3.1.

It would not be a full async support but maybe implementing sync_capable=True and being able to call an async get_response (and be called as async) could be a useful improvement of WhiteNoiseMiddleware.

It would reduce the time spent in the sync emulation layer (which is a separate thread with essentially zero concurrency because Django calls sync_to_async with the thread_sensitive=True option) and allow async views to pass through directly when not serving a static file request.

Copying the behaviour of MiddlewareMixin, it could roughly look like this:

class AsyncWhitenoiseMiddleware(WhiteNoiseMiddleware):
    sync_capable = True
    async_capable = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._async_check()

    def _async_check(self):
        if asyncio.iscoroutinefunction(self.get_response):
            self._is_coroutine = asyncio.coroutines._is_coroutine

    def __call__(self, request):
        if asyncio.iscoroutinefunction(self.get_response):
            return self.__acall__(request)
        else:
            return super().__call__(request)

    async def __acall__(self, request):
        response = self.process_request(request)
        if response is None:
            response = await self.get_response(request)
        return response

It's probably better to inherit from MiddlewareMixin instead of duplicating code.

In the previous code, process_request is called directly, which means doing disk IO in the main async thread. This is not ideal but probably not awful, I'd guess static files are very likely to be quickly available from the system cache.

bellini666 commented 1 year ago

To add to this, what I have in my projects is the following:

@sync_and_async_middleware
def whitenoise_middleware(
    get_response: Callable[[HttpRequest], HttpResponse | Coroutine[Any, Any, HttpResponse]],
):
    mid = WhiteNoiseMiddleware(get_response)

    def get_static_file(request: HttpRequest):
        # This is copied from WhiteNoiseMiddleware.__call__
        if mid.autorefresh:
            static_file = mid.find_file(request.path_info)
        else:
            static_file = mid.files.get(request.path_info)

        return mid.serve(static_file, request) if static_file is not None else None

    if inspect.iscoroutinefunction(get_response):
        aget_static_file = sync_to_async(get_static_file, thread_sensitive=False)

        async def middleware(request):  # type: ignore
            response = await aget_static_file(request)
            if response is not None:
                return response

            return await cast(Awaitable, get_response(request))

    else:

        def middleware(request):
            response = get_static_file(request)
            if response is not None:
                return response

            return get_response(request)

    return middleware

It is using the new function middleware approach.

whitenoise can probably provide something similar and deprecate the use of WhiteNoiseMiddleware