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

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

Open
aaronn opened this issue Apr 26, 2020 · 8 comments · May be fixed by #359
Open

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

aaronn opened this issue Apr 26, 2020 · 8 comments · May be fixed by #359

Comments

@aaronn
Copy link

aaronn commented Apr 26, 2020

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
Copy link
Owner

evansd commented Apr 28, 2020

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
Copy link

@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
Copy link

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
Copy link

@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
Copy link
Owner

evansd commented May 20, 2020

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
Copy link

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
Copy link

bellini666 commented Mar 5, 2023

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

@Archmonger Archmonger linked a pull request Jun 22, 2023 that will close this issue
6 tasks
@Archmonger
Copy link
Contributor

For future onlookers, ASGI support for WhiteNoise has been forked into ServeStatic.

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

Successfully merging a pull request may close this issue.

7 participants