Skip to content

Commit

Permalink
Fix compatibility with third-party sync-only middleware (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
Archmonger authored Aug 29, 2024
1 parent 04c20ac commit a486fe9
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 85 deletions.
17 changes: 0 additions & 17 deletions .editorconfig

This file was deleted.

2 changes: 2 additions & 0 deletions .github/workflows/publish-develop-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ jobs:
git config user.email [email protected]
cd docs
mike deploy --push develop
concurrency:
group: publish-docs
2 changes: 2 additions & 0 deletions .github/workflows/publish-release-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ jobs:
git config user.email [email protected]
cd docs
mike deploy --push --update-aliases ${{ github.event.release.name }} latest
concurrency:
group: publish-docs
22 changes: 11 additions & 11 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,26 @@ Using the following categories, list your changes in this order:

<!--changelog-start-->

## [Unreleased]
## [Unreleased](https://github.com/Archmonger/ServeStatic/compare/1.1.0...HEAD)

- Nothing (yet)!
### Fixed

- Fix compatibility with third-party sync only middleware
- Django middleware now only runs in async mode to avoid clashing with Django's internal usage of `asgiref.AsyncToSync`

## [1.1.0] - 2024-08-27
## [1.1.0](https://github.com/Archmonger/ServeStatic/compare/1.0.0...1.1.0) - 2024-08-27

### Added

- Files are now compressed within a thread pool to increase performance ([Upstream PR](https://github.com/evansd/whitenoise/pull/484))
- Files are now compressed within a thread pool to increase performance ([Upstream PR](https://github.com/evansd/whitenoise/pull/484))

### Fixed

- Fix Django `StreamingHttpResponse must consume synchronous iterators` warning
- Fix Django bug where file paths could fail to be followed on Windows ([Upstream PR](https://github.com/evansd/whitenoise/pull/474))
- Fix Django `StreamingHttpResponse must consume synchronous iterators` warning
- Fix Django bug where file paths could fail to be followed on Windows ([Upstream PR](https://github.com/evansd/whitenoise/pull/474))

## [1.0.0] - 2024-05-08
## [1.0.0](https://github.com/Archmonger/ServeStatic/releases/tag/1.0.0) - 2024-05-08

### Changed

- Forked from [`whitenoise`](https://github.com/evansd/whitenoise) to add ASGI support.

[Unreleased]: https://github.com/Archmonger/ServeStatic/compare/1.0.0...HEAD
[1.0.0]: https://github.com/Archmonger/ServeStatic/releases/tag/1.0.0
- Forked from [`whitenoise`](https://github.com/evansd/whitenoise) to add ASGI support.
1 change: 1 addition & 0 deletions docs/src/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ bakhit
sublicense
middleware
unhashed
async
64 changes: 12 additions & 52 deletions src/servestatic/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def _set_streaming_content(self, value):
if isinstance(value, AiofilesContextManager):
value = AsyncFileIterator(value)

# Django < 4.2 doesn't support async file responses, so convert to sync
# Django < 4.2 doesn't support async file responses, so we convert to sync
if django.VERSION < (4, 2) and hasattr(value, "__aiter__"):
value = AsyncToSyncIterator(value)

Expand All @@ -75,12 +75,15 @@ class ServeStaticMiddleware(ServeStatic):
"""

async_capable = True
sync_capable = True
sync_capable = False

def __init__(self, get_response, settings=settings):
self.get_response = get_response
if iscoroutinefunction(get_response):
markcoroutinefunction(self)
if not iscoroutinefunction(get_response):
raise ValueError(
"ServeStaticMiddleware requires an async compatible version of Django."
)
markcoroutinefunction(self)

try:
autorefresh: bool = settings.SERVESTATIC_AUTOREFRESH
Expand Down Expand Up @@ -159,32 +162,7 @@ def __init__(self, get_response, settings=settings):
if self.use_finders and not self.autorefresh:
self.add_files_from_finders()

def __call__(self, request):
if iscoroutinefunction(self.get_response):
return self.acall(request)

# Allow Django >= 3.2 to use async file responses when running via ASGI, even
# if Django forces this middleware to run synchronously
if django.VERSION >= (3, 2):
return asyncio.run(self.acall(request))

# Django version has no async uspport
return self.call(request)

def call(self, request):
"""If the URL contains a static file, serve it. Otherwise, continue to the next
middleware."""
if self.autorefresh:
static_file = self.find_file(request.path_info)
else:
static_file = self.files.get(request.path_info)
if static_file is not None:
return self.serve(static_file, request)

# Run the next middleware in the stack
return self.get_response(request)

async def acall(self, request):
async def __call__(self, request):
"""If the URL contains a static file, serve it. Otherwise, continue to the next
middleware."""
if self.autorefresh and hasattr(asyncio, "to_thread"):
Expand All @@ -197,26 +175,7 @@ async def acall(self, request):
if static_file is not None:
return await self.aserve(static_file, request)

# Run the next middleware in the stack. Note that get_response can sometimes be sync if
# middleware was run in mixed sync-async mode
# https://docs.djangoproject.com/en/stable/topics/http/middleware/#asynchronous-support
if iscoroutinefunction(self.get_response):
return await self.get_response(request)
return self.get_response(request)

@staticmethod
def serve(static_file, request):
response = static_file.get_response(request.method, request.META)
status = int(response.status)
http_response = ServeStaticFileResponse(
response.file or (),
status=status,
)
# Remove default content-type
del http_response["content-type"]
for key, value in response.headers:
http_response[key] = value
return http_response
return await self.get_response(request)

@staticmethod
async def aserve(static_file, request):
Expand Down Expand Up @@ -334,8 +293,8 @@ class AsyncToSyncIterator:
full compatibility with any environment.
This converter must create a temporary event loop in a thread for two reasons:
1) Allows us to stream the iterator instead of buffering all contents in memory.
2) Allows the iterator to be used in environments where an event loop may not exist,
1. Allows us to stream the iterator instead of buffering all contents in memory.
2. Allows the iterator to be used in environments where an event loop may not exist,
or may be closed unexpectedly.
Currently used to add async file compatibility to Django WSGI and Django versions
Expand All @@ -361,3 +320,4 @@ def __iter__(self):
loop.run_until_complete, generator.__anext__()
).result()
loop.close()
thread_executor.shutdown(wait=False)
3 changes: 1 addition & 2 deletions src/servestatic/responders.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import os
import re
import stat
from email.utils import formatdate
from email.utils import parsedate
from email.utils import formatdate, parsedate
from http import HTTPStatus
from io import BufferedIOBase
from time import mktime
Expand Down
11 changes: 8 additions & 3 deletions tests/django_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@

import django

from .utils import AppServer
from .utils import TEST_FILE_PATH
from .utils import TEST_FILE_PATH, AppServer

ALLOWED_HOSTS = ["*"]

Expand All @@ -29,7 +28,13 @@
else:
STATICFILES_STORAGE = "servestatic.storage.CompressedManifestStaticFilesStorage"

MIDDLEWARE = ["servestatic.middleware.ServeStaticMiddleware"]
MIDDLEWARE = [
"tests.middleware.sync_middleware_1",
"tests.middleware.async_middleware_1",
"servestatic.middleware.ServeStaticMiddleware",
"tests.middleware.sync_middleware_2",
"tests.middleware.async_middleware_2",
]

LOGGING = {
"version": 1,
Expand Down
38 changes: 38 additions & 0 deletions tests/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from asgiref.sync import iscoroutinefunction
from django.utils.decorators import async_only_middleware, sync_only_middleware


@sync_only_middleware
def sync_middleware_1(get_response):
def middleware(request):
response = get_response(request)
return response

return middleware


@async_only_middleware
def async_middleware_1(get_response):
async def middleware(request):
response = await get_response(request)
return response

return middleware


@sync_only_middleware
def sync_middleware_2(get_response):
def middleware(request):
response = get_response(request)
return response

return middleware


@async_only_middleware
def async_middleware_2(get_response):
async def middleware(request):
response = await get_response(request)
return response

return middleware

0 comments on commit a486fe9

Please sign in to comment.