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

Fix compatibility with third-party sync-only middleware #19

Merged
merged 10 commits into from
Aug 29, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -21,3 +21,5 @@ jobs:
git config user.email github-actions@github.com
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
@@ -21,3 +21,5 @@ jobs:
git config user.email github-actions@github.com
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
@@ -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
@@ -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
@@ -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)

@@ -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
@@ -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"):
@@ -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):
@@ -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
@@ -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
@@ -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
11 changes: 8 additions & 3 deletions tests/django_settings.py
Original file line number Diff line number Diff line change
@@ -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 = ["*"]

@@ -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,
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