diff --git a/CHANGELOG.md b/CHANGELOG.md index c3f4031e..fd9f9ff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ Using the following categories, list your changes in this order: ## [Unreleased](https://github.com/Archmonger/ServeStatic/compare/1.1.0...HEAD) +### Added + +- Verbose Django `404` error page when `settings.py:DEBUG` is `True` ([Upstream PR](https://github.com/evansd/whitenoise/pull/366)) + ### Fixed - Fix compatibility with third-party sync only middleware diff --git a/src/servestatic/middleware.py b/src/servestatic/middleware.py index 52ee2ec4..66eca968 100644 --- a/src/servestatic/middleware.py +++ b/src/servestatic/middleware.py @@ -18,6 +18,8 @@ from django.http import FileResponse from django.urls import get_script_prefix +from servestatic.responders import MissingFileError + from .asgi import BLOCK_SIZE from .string_utils import ensure_leading_trailing_slash from .wsgi import ServeStatic @@ -175,6 +177,18 @@ async def __call__(self, request): if static_file is not None: return await self.aserve(static_file, request) + if settings.DEBUG and request.path.startswith(settings.STATIC_URL): + current_finders = finders.get_finders() + app_dirs = [ + storage.location + for finder in current_finders + for storage in finder.storages.values() + ] + app_dirs = "\n• ".join(sorted(app_dirs)) + raise MissingFileError( + f"ServeStatic did not find the file '{request.path.lstrip(settings.STATIC_URL)}' within the following paths:\n• {app_dirs}" + ) + return await self.get_response(request) @staticmethod diff --git a/tests/django_urls.py b/tests/django_urls.py index 562b8d6f..157b9b17 100644 --- a/tests/django_urls.py +++ b/tests/django_urls.py @@ -1,3 +1,10 @@ from __future__ import annotations -urlpatterns = [] +from django.urls import path + + +def avoid_django_default_welcome_page(): + pass + + +urlpatterns = [path("", avoid_django_default_welcome_page)] diff --git a/tests/test_django_whitenoise.py b/tests/test_django_whitenoise.py index 590ddb70..3dfa862f 100644 --- a/tests/test_django_whitenoise.py +++ b/tests/test_django_whitenoise.py @@ -1,31 +1,33 @@ from __future__ import annotations import asyncio +import html import shutil import tempfile from contextlib import closing -from urllib.parse import urljoin -from urllib.parse import urlparse +from pathlib import Path +from urllib.parse import urljoin, urlparse import brotli import pytest from django.conf import settings -from django.contrib.staticfiles import finders -from django.contrib.staticfiles import storage +from django.contrib.staticfiles import finders, storage from django.core.asgi import get_asgi_application from django.core.management import call_command from django.core.wsgi import get_wsgi_application from django.test.utils import override_settings from django.utils.functional import empty -from .utils import AppServer -from .utils import AsgiAppServer -from .utils import AsgiReceiveEmulator -from .utils import AsgiScopeEmulator -from .utils import AsgiSendEmulator -from .utils import Files -from servestatic.middleware import ServeStaticFileResponse -from servestatic.middleware import ServeStaticMiddleware +from servestatic.middleware import ServeStaticFileResponse, ServeStaticMiddleware + +from .utils import ( + AppServer, + AsgiAppServer, + AsgiReceiveEmulator, + AsgiScopeEmulator, + AsgiSendEmulator, + Files, +) def reset_lazy_object(obj): @@ -188,7 +190,7 @@ def test_file_served_from_static_dir(finder_static_files, finder_server): def test_non_ascii_requests_safely_ignored(finder_server): - response = finder_server.get(settings.STATIC_URL + "test\u263A") + response = finder_server.get(settings.STATIC_URL + "test\u263a") assert 404 == response.status_code @@ -238,3 +240,32 @@ def test_relative_static_url(server, static_files, _collect_static): url = storage.staticfiles_storage.url(static_files.js_path) response = server.get(url) assert response.content == static_files.js_content + + +def test_404_in_prod(server): + response = server.get(settings.STATIC_URL + "garbage") + response_content = str(response.content.decode()) + response_content = html.unescape(response_content) + + assert response.status_code == 404 + assert ( + "ServeStatic did not find the file 'garbage' within the following paths:" + not in response_content + ) + + +@override_settings(DEBUG=True) +def test_error_message(server): + response = server.get(f"{settings.STATIC_URL}garbage") + response_content = str(response.content.decode()) + response_content = html.unescape(response_content) + + # Beautify for easier debugging + response_content = response_content[response_content.index("ServeStatic") :] + + assert ( + "ServeStatic did not find the file 'garbage' within the following paths:" + in response_content + ) + assert "•" in response_content + assert str(Path(__file__).parent / "test_files" / "static") in response_content