Skip to content

Instantly share code, notes, and snippets.

@graingert-coef
Created February 2, 2025 11:37
Show Gist options
  • Save graingert-coef/0e132295987d531f1e2dec40845a88c7 to your computer and use it in GitHub Desktop.
Save graingert-coef/0e132295987d531f1e2dec40845a88c7 to your computer and use it in GitHub Desktop.
"""Middleware."""
from __future__ import annotations
import logging
import pathlib
from django.conf import settings
from whitenoise.middleware import WhiteNoiseMiddleware # type: ignore[import-untyped]
logger = logging.getLogger(__name__)
IMMUTABLE_ROOT = settings.FRONTEND_DIR / "dist" / "assets"
class ImmutableReactWhiteNoiseMiddleware(WhiteNoiseMiddleware): # type: ignore[misc]
"""
Custom middleware to handle a Vite application.
Custom middleware that extends WhiteNoiseMiddleware to better handle
immutable static files in a React application. It overrides the
immutable_file_test method to mark files within a specified IMMUTABLE_ROOT
directory as immutable, allowing for more aggressive caching by browsers and
CDNs. If a file is not within IMMUTABLE_ROOT, it falls back to the default
WhiteNoise logic.
"""
def immutable_file_test(self, path: str, url: str) -> bool:
"""Return True if the path is hashed by whitenoise or in IMMUTABLE_ROOT."""
return pathlib.Path(path).is_relative_to(
IMMUTABLE_ROOT,
) or super().immutable_file_test(
path,
url,
)
import pathlib
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = pathlib.Path(__file__).resolve().parent
REPO_ROOT = BASE_DIR.parent
FRONTEND_DIR = REPO_ROOT / "frontend"
STATIC_ROOT = BASE_DIR / "staticfiles"
# ImmutableReactWhiteNoiseMiddleware is important here
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
f"{__package__}.core.middleware.ImmutableReactWhiteNoiseMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"csp.middleware.CSPMiddleware",
"djangorestframework_camel_case.middleware.CamelCaseMiddleWare",
]
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [FRONTEND_DIR / "dist"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
WHITENOISE_ROOT = FRONTEND_DIR / "dist"
INSTALLED_APPS = [
"whitenoise.runserver_nostatic",
f"{__package__}.core",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"debug_toolbar",
"drf_spectacular",
"rest_framework",
"django_linear_migrations",
]
"""The urls for the scrumblebee core app."""
from __future__ import annotations
from django.urls import re_path
from . import views
app_name = "core"
urlpatterns = [
re_path(r"^.*", views.catchall),
]
from __future__ import annotations
import urllib.parse
import httpx
from django.conf import settings
from django.http import (
HttpRequest,
HttpResponse,
HttpResponseBase,
HttpResponseRedirect,
)
from django.template import engines
from django.template.response import TemplateResponse
from django.views.generic import TemplateView
def catchall_dev(request: HttpRequest) -> HttpResponseBase:
"""Proxy the react dev-server."""
# Validate the request.path to ensure it's safe
# For example, ensure it does not contain "..", which could lead to directory
# traversal Adjust the regex pattern according to your application's routing
# scheme
uri = urllib.parse.urlsplit(
request.build_absolute_uri(request.get_full_path_info()),
)
if uri.hostname != "localhost":
port = "" if uri.port is None else f":{uri.port}"
return HttpResponseRedirect(uri._replace(netloc=f"localhost{port}").geturl())
query_string = request.META["QUERY_STRING"]
upstream = "http://localhost:5173"
try:
response = httpx.get(
upstream + request.path + (f"?{query_string}" if query_string else ""),
)
except httpx.RequestError as e:
raise RuntimeError(
f"you need to run `npm start` in the {settings.FRONTEND_DIR} directory",
) from e
content_type = response.headers.get("Content-Type")
if content_type is not None and content_type.lower() == "text/html; charset=utf-8":
tr = TemplateResponse(
template=engines["django"].from_string(response.text), # type: ignore[arg-type]
request=request,
content_type=content_type,
status=response.status_code,
)
tr.reason_phrase = response.reason_phrase
return tr
return HttpResponse(
content=response.content,
content_type=content_type,
status=response.status_code,
reason=response.reason_phrase,
)
catchall_prod = TemplateView.as_view(
template_name="index.html",
)
def catchall(request: HttpRequest) -> HttpResponseBase:
"""Catchall view for the frontend."""
return catchall_dev(request) if settings.DEVELOPMENT else catchall_prod(request)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment