djust docs
Browse documentation

Error Code Reference

Every diagnostic code djust emits, with its cause and fix.

On this page

djust uses structured error codes to help you diagnose problems quickly. This guide covers every diagnostic code djust emits as of 1.2.0rc10, what causes it, and how to fix it.


Error Code Categories

PrefixCategoryWhen Checked
C0xx, C3xx–C5xxConfiguration (C3xx: VDOM cache, C4xx: Hot View Replacement, C5xx: time travel)manage.py check --tag djust (startup)
V0xxValidationmanage.py check --tag djust (startup)
S0xxSecuritymanage.py check --tag djust (startup)
T0xxTemplatesmanage.py check --tag djust (startup)
Q0xxCode Qualitymanage.py check --tag djust (startup)
A0xxAudit / Static Security Checksmanage.py check --tag djust (startup)
D0xxDatabase notificationsmanage.py check --tag djust (startup)
U0xxUpdate noticemanage.py check --tag djust (startup, DEBUG only)
Y0xxAccessibilitymanage.py check --tag djust (startup)
djust.audio.*Audiomanage.py check --tag djust (startup)
djust_theming.*Themingmanage.py check (startup, compatibility tag)
P0xxPermissions Documentmanage.py djust_audit --permissions permissions.yaml
L0xxLive Runtime Probemanage.py djust_audit --live <url>
X0xxAST Anti-Pattern Scannermanage.py djust_audit --ast
DJE-xxxRuntimeDuring WebSocket events and VDOM diffing

Run all static checks at once:

python manage.py check --tag djust

Run the runtime probe against a deployed environment:

python manage.py djust_audit --live https://staging.example.com --strict

Validate against a committed permissions document:

python manage.py djust_audit --permissions permissions.yaml --strict

Configuration Errors (C0xx)

C001: ASGI_APPLICATION not set

Severity: Error

What causes it: Your Django settings file is missing the ASGI_APPLICATION setting. djust requires ASGI for WebSocket support.

What you see: LiveView pages load as static HTML. No WebSocket connection is established.

Fix:

# settings.py
ASGI_APPLICATION = "myproject.asgi.application"

C002: CHANNEL_LAYERS not configured

Severity: Error

What causes it: Django Channels is not configured. djust uses Channels for WebSocket communication.

What you see: WebSocket connections fail. Browser console shows WebSocket connection errors.

Fix:

# settings.py (development)
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels.layers.InMemoryChannelLayer",
    }
}

# settings.py (production -- use Redis)
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    }
}

C003: daphne ordering in INSTALLED_APPS

Severity: Warning (wrong order) / Info (no ASGI server installed)

What causes it: Either daphne is listed after django.contrib.staticfiles in INSTALLED_APPS (Warning), or daphne is not in INSTALLED_APPS and none of daphne, uvicorn or hypercorn is importable (Info: "No ASGI server detected (daphne, uvicorn, or hypercorn)."). Projects that use uvicorn or hypercorn without daphne are not flagged.

What you see: With the wrong order, manage.py runserver starts the WSGI server instead of the ASGI server, so WebSockets do not work in development.

Fix: For the Info case, install an ASGI server; pip install 'uvicorn[standard]' is recommended. For the ordering case:

# settings.py
INSTALLED_APPS = [
    "daphne",                          # Must be before staticfiles
    "django.contrib.staticfiles",
    "djust",
    # ...
]

C004: djust not in INSTALLED_APPS

Severity: Error

What causes it: The djust app is not in your INSTALLED_APPS.

What you see: Template tags, static files, and system checks are not available.

Fix:

# settings.py
INSTALLED_APPS = [
    # ...
    "djust",
    # ...
]

C005: WebSocket routes missing AuthMiddlewareStack

Severity: Warning

What causes it: Your ASGI routing does not wrap the WebSocket URL router with AuthMiddlewareStack or DjustMiddlewareStack.

What you see: request.session and request.user are unavailable inside mount(). Authentication-related features silently fail.

Fix:

# asgi.py
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(   # Wrap with this
        URLRouter(websocket_urlpatterns)
    ),
})

C010: Tailwind CDN in production

Severity: Warning

What causes it: A base/layout template includes the Tailwind CSS CDN script in a non-DEBUG environment.

What you see: Slow page loads and console warnings about Tailwind CDN usage in production.

Fix: Compile Tailwind CSS instead of using the CDN:

python manage.py djust_setup_css tailwind
# Or manually:
tailwindcss -i static/css/input.css -o static/css/output.css --minify

C011: Missing compiled CSS

Severity: Warning (production) / Info (development)

What causes it: Tailwind is configured (tailwind.config.js exists or input.css references Tailwind) but the compiled output.css is missing.

Fix: Compile the CSS:

python manage.py djust_setup_css tailwind

In development, pages render without Tailwind utilities until you compile the CSS; djust_setup_css tailwind --watch keeps it rebuilt as you edit.


C012: Manual client.js in template

Severity: Warning

What causes it: A base/layout template manually includes <script src="{% static 'djust/client.js' %}">. djust automatically injects its client JS for LiveView pages.

What you see: Double-loading of client JavaScript, which can cause race conditions and duplicate WebSocket connections.

Fix: Remove the manual <script> tag. djust handles script injection automatically.


C013: Stale collectstatic copy of client.min.js

Severity: Warning

What causes it: STATIC_ROOT is set and STATIC_ROOT/djust/client.min.js exists, but its content differs from the copy bundled with the installed djust package. Message: "Stale collectstatic copy of client.min.js detected at ... The wheel-bundled copy is different — your browser will load outdated client code."

Fix: Run python manage.py collectstatic --clear --noinput, then hard-reload the browser. If you serve client.min.js from a CDN or a custom build, suppress with DJUST_CONFIG = {"suppress_checks": ["C013"]}.


C014: django-tenants under ASGI without TENANT_LIMIT_SET_CALLS

Severity: Warning

What causes it: django-tenants is configured (django_tenants in INSTALLED_APPS or TENANT_MODEL set), ASGI_APPLICATION is set, and TENANT_LIMIT_SET_CALLS is unset or False. Every WebSocket event then emits a redundant SET search_path, which can exhaust the Postgres connection pool under LiveView load. The message also notes that django-tenants is deprecated as a multi-tenancy strategy for djust apps.

Fix: Migrate to djust's built-in row-level multi-tenancy (djust.tenants); see Multi-Tenant and Migrating from django-tenants. As a stopgap, set TENANT_LIMIT_SET_CALLS = True. Suppress with DJUST_CONFIG = {"suppress_checks": ["C014"]}.


C015: Invalid or unknown DJUST_CONFIG['extensions'] adapter

Severity: Error

What causes it: DJUST_CONFIG['extensions'] is not a list ("DJUST_CONFIG['extensions'] must be a list of adapter names, got ..."), or it names an adapter djust doesn't know ("DJUST_CONFIG['extensions'] lists unknown adapter '...'. It will be ignored -- no script is injected and the adapter's hook never mounts.").

Fix: Use a list of known adapter names, e.g. DJUST_CONFIG = {"extensions": ["chart"]}. The check's hint lists the available adapters.


C016: TEMPLATES backend order

Severity: Warning

What causes it: One of three TEMPLATES shapes:

  • a DjangoTemplates backend is listed before DjustTemplateBackend ("every template the Django engine can find is rendered by Django and never reaches djust");
  • there is a DjustTemplateBackend entry but no DjangoTemplates entry after it ("the admin / admindocs templates cannot render");
  • the admin is installed, the DjustTemplateBackend entry comes first with APP_DIRS on, and its OPTIONS['context_processors'] lacks auth, messages or request ("the admin index fails (KeyError: 'user') without the auth processor"). The djust engine renders the admin's templates in that shape, and Django's own admin checks only look at DjangoTemplates entries.

Fix: Put the DjustTemplateBackend entry first with django.template.context_processors.request, django.contrib.auth.context_processors.auth and django.contrib.messages.context_processors.messages in its context_processors, and add a django.template.backends.django.DjangoTemplates entry after it as the fallback for admin and contrib templates (the shape djust new --with-db emits). Suppress with DJUST_CONFIG = {"suppress_checks": ["C016"]}.


C018: Deprecated LIVEVIEW_CONFIG key

Severity: Warning

What causes it: LIVEVIEW_CONFIG or DJUST_CONFIG sets one of jit_cache_backend, jit_cache_dir, jit_redis_url, debug_components, component_wrapper_class or component_loading_class. djust has defaults for these keys but never reads them, so setting one has no effect.

Fix: Remove the key. djust 1.3 removes them. Suppress with DJUST_CONFIG = {"suppress_checks": ["C018"]}.


C019: Unknown PRESENCE_BACKEND

Severity: Warning

What causes it: DJUST_CONFIG['PRESENCE_BACKEND'] is set to a value djust doesn't know ("DJUST_CONFIG['PRESENCE_BACKEND'] is '...', which djust does not know; presence falls back to the in-memory backend (one process only)."). A dotted class path counts as unknown: the setting takes a short name.

Fix: Use 'redis' or 'tenant_redis' for presence shared between processes, or 'memory' / 'tenant_memory' for a single process. Suppress with DJUST_CONFIG = {"suppress_checks": ["C019"]}.


C301: Invalid VDOM cache TTL

Severity: Error

What causes it: DJUST_VDOM_CACHE_TTL_SECONDS (or LIVEVIEW_CONFIG["service_worker"]["vdom_cache_ttl_seconds"]) is not a positive integer. Message: "DJUST_VDOM_CACHE_TTL_SECONDS must be a positive integer." A TTL <= 0 disables expiry, so the service worker could serve indefinitely stale HTML on back-navigation.

Fix: Set a positive number of seconds (the default is 1800).


C302: Invalid VDOM cache size

Severity: Error

What causes it: DJUST_VDOM_CACHE_MAX_ENTRIES (or LIVEVIEW_CONFIG["service_worker"]["vdom_cache_max_entries"]) is below 1. Message: "DJUST_VDOM_CACHE_MAX_ENTRIES must be >= 1." A max of 0 evicts every entry on insertion and silently disables the cache.

Fix: Set it to 1 or more (the default is 50).


C303: VDOM cache disabled

Severity: Info

What causes it: DJUST_VDOM_CACHE_ENABLED (or LIVEVIEW_CONFIG["service_worker"]["vdom_cache_enabled"]) is False. Message: "DJUST_VDOM_CACHE_ENABLED is False; VDOM cache disabled." Back-navigation falls through to a fresh mount and render instead of an instant paint.

Fix: Nothing is broken. Re-enable the cache, or suppress with DJUST_CONFIG = {"suppress_checks": ["C303"]}.


C304: State snapshot with PII-like attribute names

Severity: Warning

What causes it: A LiveView sets enable_state_snapshot = True and declares public class attributes or annotations whose names look sensitive (matching password, token, secret, api_key, pii, ssn, credit_card, bearer, private_key, auth_header, sensitive or credential). Message: "<view>: enable_state_snapshot=True with PII-like attribute names: ...". State snapshots are cached client-side by the service worker, so these values would be stored in browser cache storage.

Fix: Make the attributes private (leading _), or turn off enable_state_snapshot for this view.


C401: Hot View Replacement without watchdog

Severity: Warning (DEBUG only)

What causes it: Hot View Replacement and hot reload are enabled (the defaults), but the watchdog package is not installed. Message: "Hot View Replacement is enabled but watchdog is not installed." Code changes won't hot-swap live view instances.

Fix: pip install watchdog, or disable HVR. See Hot View Replacement.


C501: Time-travel debugging enabled globally

Severity: Info (DEBUG only)

What causes it: LIVEVIEW_CONFIG['time_travel_enabled'] is True. Message: "Time-travel debugging is enabled globally (LIVEVIEW_CONFIG['time_travel_enabled']=True)." This is a discoverability notice; individual views still need time_travel_enabled = True to allocate a buffer.

Fix: None needed. See Time-Travel Debugging.


C502: Invalid time_travel_max_events

Severity: Error (DEBUG only)

What causes it: time_travel_max_events in the djust config is not a positive integer. Message: "time_travel_max_events must be a positive integer (got ...)." The time-travel ring buffer raises ValueError on a non-positive cap, which breaks LiveView.__init__ for any view with time_travel_enabled = True.

Fix: Set a positive integer (the default is 100).


Validation Errors (V0xx)

V001: Missing template_name

Severity: Warning

What causes it: A LiveView subclass does not define template_name (and no parent class defines it either).

Fix:

class MyView(LiveView):
    template_name = "myapp/my_template.html"  # Add this

Abstract base classes: if the class is an abstract base never mounted directly (no template_name by design), mark it explicitly:

class BaseLiveView(LiveView):
    abstract = True   # skips all per-class V0xx/Q0xx checks for this class (not inherited)
    login_required = True

Global suppression (less preferred — turns the check off everywhere):

# settings.py
DJUST_CONFIG = {"suppress_checks": ["V001"]}  # honored as of #1604

V002: Missing mount() method

Severity: Info

What causes it: A LiveView subclass does not define a mount() method. While not strictly required, mount() is where you initialize state.

Fix:

class MyView(LiveView):
    template_name = "my_template.html"

    def mount(self, request, **kwargs):
        self.count = 0

V003: Wrong mount() signature

Severity: Error

What causes it: The mount() method does not accept request as its first positional argument.

Fix:

# WRONG
def mount(self):
    pass

# WRONG
def mount(self, **kwargs):
    pass

# CORRECT
def mount(self, request, **kwargs):
    self.count = 0

V004: Missing @event_handler decorator

Severity: Info

What causes it: A public method name matches event handler naming patterns (e.g., on_*, toggle_*, select_*, update_*, delete_*, create_*, add_*, remove_*, save_*, cancel_*, submit_*, close_*, open_*) but is not decorated with @event_handler. handle_* methods are not flagged: an undecorated handle_* method is the way to write a handler that server push can call and browsers cannot.

Without the decorator, the method cannot be called from templates via dj-click or other directives.

Fix:

from djust.decorators import event_handler

class MyView(LiveView):
    @event_handler()
    def toggle_sidebar(self, **kwargs):
        self.sidebar_open = not self.sidebar_open

If the method is intentionally private (not callable from templates), prefix it with _:

def _toggle_internal_flag(self):
    self._flag = not self._flag

V005: Module not in LIVEVIEW_ALLOWED_MODULES

Severity: Warning

What causes it: You have set LIVEVIEW_ALLOWED_MODULES in settings and a LiveView's module is not in the list. WebSocket mount will silently fail for this view.

Fix:

# settings.py
LIVEVIEW_ALLOWED_MODULES = [
    "myapp.views",
    "otherapp.views",  # Add the missing module
]

Or remove LIVEVIEW_ALLOWED_MODULES entirely to allow all modules.

Abstract base classes: mark abstract base views with abstract = True to skip V005 (and every other per-class V0xx/Q0xx check) for that specific class:

class BaseLiveView(LiveView):
    abstract = True   # skips all per-class V0xx/Q0xx checks for this class

Global suppression: DJUST_CONFIG = {"suppress_checks": ["V005"]} (honored as of #1604).


V006: Service instance in mount()

Severity: Warning

What causes it: AST analysis detected an assignment in mount() that instantiates a class whose name contains keywords like "Service", "Client", "Session", "API", or "Connection". These are typically not JSON-serializable.

What you see: The view works initially, but after a WebSocket reconnection or page refresh, you get AttributeError when trying to call methods on what is now a string representation of the object.

Fix: Use the helper method pattern:

# WRONG
class MyView(LiveView):
    def mount(self, request, **kwargs):
        self.s3 = boto3.client("s3")  # Not serializable

# CORRECT
class MyView(LiveView):
    def _get_s3(self):
        return boto3.client("s3")

    def mount(self, request, **kwargs):
        s3 = self._get_s3()  # Temporary variable, not stored in state
        # Use s3 here to fetch data

Or suppress if you know the object is serializable:

self.api_client = MySerializableClient()  # noqa: V006

Related: Working with External Services


V007: Event handler missing **kwargs

Severity: Warning

What causes it: An @event_handler decorated method does not include **kwargs in its signature. Event handlers receive all event parameters from the client, and without **kwargs, extra parameters will cause errors.

Fix:

# WRONG - will fail if client sends unexpected parameters
@event_handler()
def search(self, query: str = ""):
    self.results = search(query)

# CORRECT
@event_handler()
def search(self, query: str = "", **kwargs):
    self.results = search(query)

V008: Non-primitive type in mount()

Severity: Info

What causes it: AST analysis detected an assignment in mount() that instantiates a non-primitive type (not list, dict, set, tuple, str, int, float, bool). This may indicate a non-serializable object being stored in LiveView state.

What you see: Similar to V006, but catches a broader range of types. You may see runtime warnings about non-serializable values, or AttributeError after deserialization.

Fix: If the type is not JSON-serializable, use a private variable or helper method:

# WRONG
class MyView(LiveView):
    def mount(self, request, **kwargs):
        self.processor = DataProcessor()  # May not be serializable

# CORRECT - private variable
class MyView(LiveView):
    def mount(self, request, **kwargs):
        self._processor = DataProcessor()  # Private, not serialized

# OR - if it IS serializable, suppress the check
class MyView(LiveView):
    def mount(self, request, **kwargs):
        self.config = MySerializableConfig()  # noqa: V008

V008 is broader than V006 and will flag any custom class instantiation, not just service-like names. This helps catch subtle serialization bugs early.

Related: Working with External Services


V009: Invalid on_mount hooks

Severity: Warning

What causes it: A LiveView's on_mount is not a list or tuple ("<view>: 'on_mount' should be a list of hook functions."), or one of its entries is not callable ("<view>: on_mount[i] is not callable (...).").

Fix: Set on_mount = [hook1, hook2], where each entry is a callable hook function.


V010: TutorialMixin after LiveView in the bases

Severity: Error

What causes it: A class lists LiveView before TutorialMixin in its bases. Message: "<view>: TutorialMixin must be listed before LiveView in bases." Django's View.__init__ does not call super().__init__(), so mixins listed after LiveView are never initialised.

Fix: Change class MyView(LiveView, TutorialMixin) to class MyView(TutorialMixin, LiveView).


V011: Sticky child snapshots state but its parent doesn't

Severity: Warning

What causes it: A view embedded as a sticky child sets enable_state_snapshot = True, but the parent that embeds it does not. Message: "<child>: used as a sticky child with enable_state_snapshot=True, but embedding parent <parent> does not opt in — the child's state will be silently dropped on reconnect." A sticky child is restored across a WebSocket reconnect only when both the child and its embedding parent opt in.

Fix: Set enable_state_snapshot = True on the parent as well, or suppress with DJUST_CONFIG = {"suppress_checks": ["V011"]}. See Sticky Child Persistence.


V012: Sticky child template declares its own dj-view

Severity: Warning

What causes it: A view embedded with {% live_render ... sticky=True %} has a template whose root declares dj-view. Message: "<view>: sticky child template declares its own 'dj-view' on its root — this nests a duplicate dj-view inside the sticky wrapper and breaks the child's client-side mount (its events won't bind)." The framework emits the wrapper element with the dj-view binding itself.

Fix: Remove dj-view from the sticky child's root element. See Sticky LiveViews.


V013: dispatch()/get()/post() override that won't run over WebSocket

Severity: Warning

What causes it: A LiveView (or one of its ancestors) overrides dispatch(), get() or post(). Message: "<view>: <ancestor> overrides <methods>, which will NOT run on a WebSocket mount (the WS path calls mount() directly, never dispatch()/get()/post())."

Fix: Move the setup into mount(self, request, **kwargs) so it runs on every transport. Mark abstract base classes with abstract = True, or suppress with DJUST_CONFIG = {"suppress_checks": ["V013"]}.


V014: Time travel records PII-like fields

Severity: Warning

What causes it: A view sets time_travel_enabled = True, and its model or form declares fields whose names look like PII that are not listed in time_travel_excluded_fields. Message: "<view>: time_travel_enabled = True, and its model/form declares field(s) whose names look like PII and are not in time_travel_excluded_fields: ...". Time-travel snapshots can be exported as a shareable bug-capture blob.

Fix: List the sensitive public-state keys in time_travel_excluded_fields on the view, or suppress with DJUST_CONFIG = {"suppress_checks": ["V014"]} if they never reach the view's public state. See Bug Capture.


V015: djust's own LiveViews blocked by LIVEVIEW_ALLOWED_MODULES

Severity: Warning

What causes it: LIVEVIEW_ALLOWED_MODULES is set, your URLconf routes a LiveView that djust ships (the component gallery, the theme gallery, the admin extension), and the list doesn't admit it. An explicit list replaces the default, which includes "djust", so those pages render but never mount ("View not mounted. Please reload the page."). V005 doesn't cover this case because it skips classes defined in djust.

Fix: Add "djust" to LIVEVIEW_ALLOWED_MODULES, as djust new does since 1.2.1. Suppress with DJUST_CONFIG = {"suppress_checks": ["V015"]}.


Security Errors (S0xx)

S001: mark_safe() with f-string

Severity: Error

What causes it: Using mark_safe(f"...") with interpolated values. This is a cross-site scripting (XSS) vulnerability.

Fix:

# WRONG -- XSS vulnerability
from django.utils.safestring import mark_safe
html = mark_safe(f"<b>{user_input}</b>")

# CORRECT
from django.utils.html import format_html
html = format_html("<b>{}</b>", user_input)

S002: @csrf_exempt without justification

Severity: Warning

What causes it: A view function uses @csrf_exempt without a docstring explaining why CSRF protection is disabled.

Fix: Add a docstring mentioning "csrf" to acknowledge the exemption:

@csrf_exempt
def webhook_endpoint(request):
    """CSRF exempt: external webhook callback from payment provider."""
    pass

Or suppress the check on a specific line:

@csrf_exempt  # noqa: S002
def my_view(request):
    pass

S003: Bare except: pass

Severity: Warning

What causes it: A bare except: pass block that swallows all exceptions silently.

Fix:

# WRONG
try:
    do_something()
except:
    pass

# CORRECT -- catch specific exception and log
try:
    do_something()
except ValueError as e:
    logger.warning("Failed: %s", e)

S004: DEBUG=True with non-localhost ALLOWED_HOSTS

Severity: Warning

What causes it: DEBUG = True but ALLOWED_HOSTS includes non-local addresses. This exposes detailed error pages to external users.

Fix: Set DEBUG = False in production, or restrict ALLOWED_HOSTS to local addresses during development.


S005: State exposed without authentication

Severity: Warning

What causes it: A LiveView exposes public state variables but has no authentication configured (no login_required, permission_required, or check_permissions override).

Fix:

class AdminDashboardView(LiveView):
    login_required = True  # Require login
    # or
    permission_required = "myapp.view_dashboard"  # Require specific permission

    template_name = "dashboard.html"

If the view is intentionally public, acknowledge it explicitly:

class PublicCounterView(LiveView):
    login_required = False  # Explicitly public

Note: Prior to #303, this check incorrectly warned on views with login_required = False. The check now correctly distinguishes between intentionally public views (False) and views that haven't addressed authentication at all (None).

Related: Authentication Guide


S006: Tenant isolation is fail-open

Severity: Warning

What causes it: DJUST_TENANTS['STRICT_MODE'] is set to False. Message: "DJUST_TENANTS['STRICT_MODE'] is set to False — djust tenant isolation is fail-OPEN. Tenant-scoped queries that run without a tenant bound to the current context will return EVERY tenant's rows instead of an empty set, risking cross-tenant data disclosure (especially on the WebSocket/SSE live path)."

Fix: Remove STRICT_MODE (or set it to True) to keep fail-closed isolation. If you must keep it, scope every query explicitly (Model.objects.unscoped(reason=...) for deliberate cross-tenant reads). See Multi-Tenant.


S007: Unsafe rendering of a client-supplied filename

Severity: Warning

What causes it: A template renders an upload entry's client_name with the |safe filter, e.g. {{ upload_entry.client_name|safe }}. The client_name is the attacker-controlled original filename of an upload, stored without sanitisation; |safe disables Django's auto-escaping, so a filename containing <script>...</script> renders as live HTML — a stored XSS vector.

Fix: Remove the |safe filter so auto-escaping (the safe default) applies:

<!-- Unsafe: filename is attacker-controlled -->
{{ upload_entry.client_name|safe }}

<!-- Safe: auto-escaping neutralises HTML in the filename -->
{{ upload_entry.client_name }}

If the value is genuinely pre-sanitised server-side, escape it explicitly with django.utils.html.escape() before rendering, or suppress the check:

# settings.py — only if client_name is pre-sanitised
DJUST_CONFIG = {"suppress_checks": ["S007"]}  # or "djust.S007"

Detection: matches {{ <expr>.client_name|safe }} (whitespace around | tolerated); word-boundary guards mean notclient_name and client_name_foo do not trigger it.

Related: #1821, S001 (mark_safe() with f-string)


S008: Upload client_name used in a storage path

Severity: Warning

What causes it: Python code uses an upload entry's client_name in a storage path or key. Message: "<file>:<line> -- upload client_name used in a storage path/key. client_name is the raw attacker-controlled original filename (path/object-key injection: CWE-22 / CWE-73)."

Fix: Use entry.safe_client_name (basename only, traversal neutralised) for the path or key, and keep client_name for display only. Suppress with DJUST_CONFIG = {"suppress_checks": ["S008"]} if the value is pre-sanitised. See Uploads.


S009: View-level auth with ungated public handlers

Severity: Warning

What causes it: A LiveView declares view-level auth (login_required, permission_required or a Django auth mixin) and exposes a public @event_handler with no per-handler authorization gate. Message: "<file>:<line> -- LiveView '<View>' declares view-level auth but exposes the public @event_handler '<handler>' with no per-handler authorization gate. A user who passes the view's mount auth can call this handler."

Fix: If the handler needs finer authorization, add @permission_required(...) to it or a check_permissions() override that inspects the event. Rename it with a leading underscore if it isn't meant to be client-callable. If view-level auth is sufficient, suppress with # noqa: S009 on the handler or DJUST_CONFIG = {"suppress_checks": ["S009"]}.


S011: Inline script inside a LiveView root without a CSP

Severity: Warning

What causes it: A template has an executable inline <script> inside a dj-root/dj-view subtree, and no Content-Security-Policy setting is configured. Message: "<file>:<line> -- inline <script> with executable JS inside a LiveView template and no Content-Security-Policy is configured. Inline scripts inside the dj-root are not re-executed after djust morphs the mount HTML (#1848), and a strict CSP would block them."

Fix: Move the JS into a static module, or into a base-template block rendered after the dj-root closes. If the inline script is intentional, add a CSP nonce or place it outside the root. Suppress with {# noqa: S011 #} on the script line or DJUST_CONFIG = {"suppress_checks": ["S011"]}.


S012: Auth in dispatch() is not enforced over WebSocket

Severity: Error

What causes it: A LiveView gates auth with @method_decorator(..., name="dispatch"), or overrides dispatch() with auth logic. Either is enforced only on the HTTP GET, not on the WebSocket. Message: "<file>:<line> -- LiveView '<View>' gates auth via @method_decorator(..., name='dispatch'); this is NOT enforced over WebSocket (only on the HTTP GET)." (or "... overrides dispatch() with auth logic; ...").

Fix: Use djust's login_required / permission_required class attributes, a check_permissions() method, or a Django auth mixin (LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin). These are honored on every transport. See Authentication.


Template Errors (T0xx)

T001: Deprecated @event syntax

Severity: Warning

What causes it: A template uses the old @click, @input, @change, or @submit syntax instead of the current dj-click, dj-input, dj-change, dj-submit.

Fix:

<!-- WRONG (deprecated) -->
<button @click="increment">+1</button>

<!-- CORRECT -->
<button dj-click="increment">+1</button>

T002: Missing dj-root

Severity: Info

What causes it: A template contains djust directives (dj-click, dj-input, dj-change, dj-submit, dj-model) or a dj-view attribute, but no element has the dj-root attribute. The check skips templates that use {% extends %} since the root may be in a parent template.

What you see: Nothing breaks. dj-root is auto-inferred from dj-view on both the client and the server, so this is informational.

Fix: Optionally add dj-root next to dj-view for clarity, or suppress the check with DJUST_CONFIG = {"suppress_checks": ["T002"]}:

<div dj-view="myapp.views.MyView" dj-root>
    <!-- content -->
</div>

Related: Template Requirements Guide


T003: Wrapper template using {% include %} instead of {{ liveview_content|safe }}

Severity: Info

What causes it: A wrapper template that references LiveView content uses {% include %} tags instead of the {{ liveview_content|safe }} variable.

Fix: Use {{ liveview_content|safe }} in wrapper templates to render LiveView content.


T004: document.addEventListener for djust events

Severity: Warning

What causes it: A template uses document.addEventListener('djust:...') for a djust custom event that is dispatched on window (e.g. djust:push_event, djust:before-navigate, djust:error, djust:shell-swapped, djust:vdom-cache-applied, djust:upload:*).

What you see: The event listener never fires. No error in the console -- completely silent failure.

Exempt (#1809): djust dispatches a second family of events on document (not window): djust:navigate-start, djust:navigate-end, djust:hvr-applied, djust:layout-changed, djust:ws-reconnected, djust:time-travel-state, djust:time-travel-event. Listening for those on document is correct and is no longer flagged by T004.

Fix:

<!-- WRONG -- window-dispatched event, never fires on document -->
<script>
document.addEventListener('djust:push_event', (e) => { ... });
</script>

<!-- CORRECT -->
<script>
window.addEventListener('djust:push_event', (e) => { ... });
</script>

<!-- ALSO CORRECT -- navigate-end is dispatched on document, not window -->
<script>
document.addEventListener('djust:navigate-end', (e) => { ... });
</script>

Suppress globally: DJUST_CONFIG = {"suppress_checks": ["T004"]} (works as of #1809) or SILENCED_SYSTEM_CHECKS = ["djust.T004"].


T005: dj-view and dj-root on different elements

Severity: Warning

What causes it: A template has dj-view on one element and dj-root on a different element. These attributes must be on the same root element.

What you see: VDOM patches may not be applied correctly, or updates fail silently.

Fix:

<!-- WRONG -- different elements -->
<div dj-view="myapp.views.MyView">
    <div dj-root>
        <p>Content</p>
    </div>
</div>

<!-- CORRECT -- same element -->
<div dj-view="myapp.views.MyView" dj-root>
    <p>Content</p>
</div>

T010: dj-click used for navigation

Severity: Warning

What causes it: A template element uses dj-click together with navigation-related data attributes (data-view, data-tab, data-page, or data-section). This pattern suggests you're implementing navigation, which should use dj-patch instead for proper URL updates and browser history support.

What you see: Navigation works but the browser URL doesn't update, and the back button doesn't work as expected. Users cannot bookmark or share specific views.

Fix:

<!-- WRONG -- dj-click for navigation -->
<button dj-click="show_settings" data-view="settings">Settings</button>

<!-- CORRECT -- use dj-patch for navigation -->
<a href="?view=settings" dj-patch>Settings</a>

A bare dj-patch on an <a> patches to its href. (A non-empty dj-patch value is itself the target URL.)

In your LiveView, read the URL parameter in the handle_params() lifecycle hook. It is not an event handler, so don't decorate it with @event_handler:

class MyView(LiveView):
    def mount(self, request, **kwargs):
        self.current_view = "dashboard"

    def handle_params(self, params, uri):
        # Called after mount and on every dj-patch / back-forward URL change
        self.current_view = params.get("view", "dashboard")

Related: Navigation Guide


T011: Unsupported template tag

Severity: Warning

What causes it: A LiveView template uses a Django template tag that the Rust renderer doesn't implement. Message: "<file>:<line> -- unsupported template tag '{% <tag> %}' will be silently ignored by Rust renderer." In 1.2.0rc10 the unsupported-tag set is empty (every tag Django registers is handled), so this check does not currently fire.

Fix: Pre-compute the value in your view and pass it as a context variable, or use a supported alternative. Suppress with {# noqa: T011 #}.


T012: dj-* directives without dj-view

Severity: Warning

What causes it: A template uses dj-* event directives but has no dj-view attribute. Message: "<file> -- template uses dj-* event directives but has no dj-view attribute."

Fix: Add dj-view="yourapp.views.YourView" to the root element. If the template is an intentional fragment included from a parent LiveView root, add a {# djust:partial #} comment, or suppress with DJUST_CONFIG = {"suppress_checks": ["T012"]}.


T013: Invalid dj-view value

Severity: Warning

What causes it: A dj-view attribute has an empty or invalid value ("<file>:<line> -- dj-view has empty or invalid value '...'."), or it names a context variable djust never provides, such as dj-view="{{ view_path }}" ("... names a context variable djust never provides; it renders as dj-view="" and the page cannot mount.").

Fix: Write the literal dotted path, e.g. dj-view="myapp.views.MyView", or put dj-root on the element and let djust stamp dj-view server-side.


T014: Deprecated data-dj-id attribute

Severity: Warning

What causes it: A template contains data-dj-id. Message: "<file>:<line> -- deprecated 'data-dj-id' attribute (renamed to 'dj-id' in v1.0)."

Fix: Replace data-dj-id with dj-id in hand-authored HTML.


T015: Legacy data-djust-view / data-djust-root attribute

Severity: Warning

What causes it: A template uses the pre-1.0 root attributes. Message: "<file>:<line> -- legacy '<attr>' attribute detected."

Fix: Change data-djust-view to dj-view and data-djust-root to dj-root. Suppress with DJUST_CONFIG = {"suppress_checks": ["T015"]}.


T016: dj-navigate with no LiveView routes

Severity: Warning

What causes it: Templates use dj-navigate, but no LiveView routes were found in the URLconf, so the client route map is empty. Message: "dj-navigate is used in N location(s) (first: <file>:<line>) but no LiveView routes were found in the URLconf, so the client route map is empty — dj-navigate will silently full-reload instead of navigating over the WebSocket."

Fix: Make sure your views subclass djust.LiveView and are wired into urlpatterns. Suppress with DJUST_CONFIG = {"suppress_checks": ["T016"]}. See Navigation.


T017: dj-view or dj-root on a table-section element

Severity: Warning

What causes it: dj-view or dj-root is on a <tbody>, <thead>, <tfoot>, <tr>, <td>, <th>, <caption>, <col> or <colgroup>. Message: "<file>:<line> -- '<attr>' is on a <tag> table-section element, which is foster-parented out of the tree at render time (the rendered output silently drops the table rows, with no error)."

Fix: Put the attribute on a wrapping element (the <table> or a surrounding <div>). Suppress with DJUST_CONFIG = {"suppress_checks": ["T017"]}.


T018: Undefined template variable

Severity: Warning (undefined variable) / Info (templates skipped)

What causes it: A LiveView's template references a variable that is never set via a class attribute, a self.<name> = ... assignment, or a literal get_context_data() return key, and isn't a framework- or Django-injected name. Message: "<view> -- template references undefined variable '<name>' at line N (<template>) -- it resolves to nothing and renders as empty string, with no error." Views whose templates use {% extends %} are skipped, and one Info message reports the count ("T018: skipped N view(s) whose template(s) use {% extends %} ...").

Fix: Fix the typo, or, if the variable is set dynamically, silence it with a {# djust_typecheck: noqa <name> #} template comment. For extends-based templates, run manage.py djust_typecheck. Suppress globally with DJUST_CONFIG = {"suppress_checks": ["T018"]}. See Template type checking.


Code Quality (Q0xx)

Q001: print() statement

Severity: Info

What causes it: A print() call was found in production code.

Fix: Use the logging module instead:

import logging
logger = logging.getLogger(__name__)

# WRONG
print(f"Processing {item}")

# CORRECT
logger.info("Processing %s", item)

Q002: f-string in logger call

Severity: Warning

What causes it: A logger call uses an f-string instead of %s-style formatting. f-strings are evaluated eagerly, even when the log level is disabled.

Fix:

# WRONG -- f-string evaluated even if DEBUG is off
logger.debug(f"Processing {expensive_repr(item)}")

# CORRECT -- lazy evaluation
logger.debug("Processing %s", item)

Q003: console.log without djustDebug guard

Severity: Info

What causes it: A JavaScript file contains console.log without a djustDebug check. djust's client JS should only log when debugging is explicitly enabled.

Fix:

// WRONG
console.log("Connected to", url);

// CORRECT
if (globalThis.djustDebug) {
    console.log("Connected to", url);
}

Q007: Key in both static_assigns and temporary_assigns

Severity: Warning

What causes it: A LiveView lists the same keys in static_assigns and temporary_assigns. Message: "<view>: keys ... appear in both static_assigns and temporary_assigns." A key can't be both static (never re-sent) and temporary (cleared after render).

Fix: Remove the key from one of the two lists.


Q010: Navigation state set without patch()

Severity: Info

What causes it: An event handler sets a navigation-state attribute (active_view, current_tab, selected_page, current_section, active_tab or selected_view) that the view also uses as a URL parameter elsewhere via self.patch(), but this handler updates it without patch() or handle_params. Message: "<file>:<line> -- Event handler '<View>.<handler>()' sets <attr> without using patch(). Consider using dj-patch for URL updates."

Fix: Use dj-patch="?tab=value" and read the value in handle_params() (see T010), or add # noqa: Q010 on the handler.


Audit / Static Security Checks (A0xx)

These checks were added as follow-ups to the 2026-04-10 a downstream consumer penetration test. They extend djust_audit / djust_check with configuration-level security checks that catch misconfigurations Django's own check --deploy cannot see. All A0xx checks fire during manage.py check --tag djust and appear alongside the C0xx/S0xx findings.

A001: WebSocket router missing AllowedHostsOriginValidator

Severity: Error

What causes it: The "websocket" entry in your ProtocolTypeRouter is not wrapped in channels.security.websocket.AllowedHostsOriginValidator. Any cross-origin page on the internet can open a WebSocket to your app, mount any LiveView, and dispatch events from a victim's browser (CSWSH — see #653).

What you see: No runtime symptom until a pentester or attacker finds it. An A001 warning from manage.py check at startup.

Fix:

# asgi.py
from channels.routing import ProtocolTypeRouter, URLRouter
from djust.routing import DjustMiddlewareStack   # Wraps in AllowedHostsOriginValidator by default since 0.4.1

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": DjustMiddlewareStack(
        URLRouter(websocket_urlpatterns)
    ),
})

Or wrap manually if you're using channels.auth.AuthMiddlewareStack:

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator

application = ProtocolTypeRouter({
    "websocket": AllowedHostsOriginValidator(
        AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
    ),
})

Prerequisite: settings.ALLOWED_HOSTS must not contain "*".


A010: ALLOWED_HOSTS is only ["*"] in production

Severity: Error

What causes it: settings.ALLOWED_HOSTS == ["*"] with DEBUG=False. The wildcard disables Django's Host header defense entirely, and re-opens CSWSH because AllowedHostsOriginValidator reads the same setting.

Not raised when DEBUG=True, or when both SECURE_PROXY_SSL_HEADER and DJUST_TRUSTED_PROXIES are set (you are asserting a trusted L7 proxy).

Fix: Set ALLOWED_HOSTS to the explicit hostnames your app serves:

# settings.py
ALLOWED_HOSTS = ["myapp.example.com", "api.example.com"]

A011: ALLOWED_HOSTS mixes "*" with explicit hosts

Severity: Error

What causes it: settings.ALLOWED_HOSTS = ["myapp.example.com", "*"]. Django accepts any Host header as soon as "*" is present — the explicit hostname is meaningless once the wildcard is in the list. Authors often mix them thinking they're "also allowing the explicit host for clarity."

Not raised when DEBUG=True, or when both SECURE_PROXY_SSL_HEADER and DJUST_TRUSTED_PROXIES are set (you are asserting a trusted L7 proxy).

Fix: Remove "*" and keep only the explicit hostnames.


A012: USE_X_FORWARDED_HOST=True + wildcard ALLOWED_HOSTS

Severity: Error

What causes it: USE_X_FORWARDED_HOST=True makes Django trust the X-Forwarded-Host header. Combined with wildcard ALLOWED_HOSTS, there is no validation of that header — attackers can inject any Host.

Not raised when DEBUG=True, or when both SECURE_PROXY_SSL_HEADER and DJUST_TRUSTED_PROXIES are set (you are asserting a trusted L7 proxy).

Fix: Set ALLOWED_HOSTS to explicit hostnames, or set USE_X_FORWARDED_HOST=False if your reverse proxy is not configured to set it safely.


A014: SECRET_KEY starts with "django-insecure-" in production

Severity: Error

What causes it: The Django scaffold default SECRET_KEY is a placeholder that starts with "django-insecure-". It's meant to be replaced before deployment. An attacker who knows the value (anyone with access to the source repo) can forge session cookies and password-reset tokens.

Fix: Generate a new key and load it from an environment variable:

import os
from django.core.management.utils import get_random_secret_key

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY") or get_random_secret_key()

Then set DJANGO_SECRET_KEY in your deployment environment.


A020: LOGIN_REDIRECT_URL hardcoded with multi-group auth

Severity: Warning

What causes it: LOGIN_REDIRECT_URL is a single hardcoded path (e.g. /dashboard/) but the project uses a role-based auth model (detected via known packages like rolepermissions, rules, guardian, or via multiple entries in the Group table). All roles land on the same page after login — both a UX problem and a strong signal that per-role access control wasn't considered.

What you see: A lower-privilege user logs in and lands on the same dashboard as a supervisor, immediately exposing any broken RBAC. This is how a downstream consumer pentest team found broken role isolation in minutes.

Fix: Subclass django.contrib.auth.views.LoginView and override get_success_url():

<!-- The import is correct Django; the checker's minimal tests.settings cannot load django.contrib.auth.views. -->
from django.contrib.auth.views import LoginView
from django.urls import reverse

class RoleAwareLoginView(LoginView):
    def get_success_url(self):
        user = self.request.user
        if user.groups.filter(name="Supervisor").exists():
            return reverse("supervisor-dashboard")
        if user.groups.filter(name="Examiner").exists():
            return reverse("examiner-dashboard")
        return reverse("claimant-dashboard")

A030: django.contrib.admin without brute-force protection

Severity: Warning

What causes it: django.contrib.admin is in INSTALLED_APPS but no known brute-force protection package is present. The stock Django admin has no built-in rate limiting or lockout.

Fix: Install one of the recognized packages:

pip install django-axes
# settings.py
INSTALLED_APPS = [
    # ...
    "django.contrib.admin",
    "axes",
]

MIDDLEWARE = [
    # ...
    "axes.middleware.AxesMiddleware",
]

Recognized packages: axes, defender, brutebuster, ratelimit, django_ratelimit, django_axes.


A031: Observability endpoints without LocalhostOnlyObservabilityMiddleware

Severity: Warning

What causes it: The djust observability URLs (_djust/observability/) are wired, but LocalhostOnlyObservabilityMiddleware is not in MIDDLEWARE. Message: "djust observability endpoints (_djust/observability/) are wired but LocalhostOnlyObservabilityMiddleware is not in MIDDLEWARE." The endpoints also gate themselves to localhost, so this is defense in depth.

Fix: Add the middleware (under DEBUG), which rejects non-localhost requests before the view runs.


A070: dj_activity without a name

Severity: Warning

What causes it: A {% dj_activity %} block has no name argument. Message: "<file>:<line> -- {% dj_activity %} is missing a 'name' argument." Without a name, ActivityMixin cannot route events or track visibility for the region.

Fix: Give every block a non-empty name: {% dj_activity "my-panel" visible=expr %}. See Activity.


A071: Duplicate dj_activity name

Severity: Error

What causes it: Two {% dj_activity %} blocks in one template share a name. Message: "<file>:<line> -- duplicate {% dj_activity %} name '<name>' (first declared at line N)."

Fix: Rename one of the blocks, or split the template if the regions should be tracked independently.


A072: Non-LiveView class in an admin widget slot

Severity: Warning

What causes it: A djust admin change_form_widgets or change_list_widgets slot contains a class that isn't a LiveView subclass. Message: "Admin <site> -- <slot> on <model> contains non-LiveView class '<name>'. Widget slots can only embed djust LiveView subclasses."

Fix: Make the widget a subclass of djust.LiveView, or remove it from the slot. See Admin Widgets.


A073: Progress actions with multiple ASGI workers

Severity: Info

What causes it: An admin site uses @admin_action_with_progress and DJUST_ASGI_WORKERS is greater than 1. The progress widget keeps job state in a process-local dict, so the progress URL must reach the worker that started the job.

Fix: Run a single ASGI worker, or enable sticky sessions on your load balancer. Unset DJUST_ASGI_WORKERS (or set it to 1) to silence the check. See Admin Widgets.


A075: live_render with both sticky=True and lazy=True

Severity: Warning

What causes it: A {% live_render %} tag passes both sticky=True and lazy=True. Message: "<file>:<line> -- {% live_render %} has both sticky=True and lazy=True — these kwargs are mutually exclusive." Sticky reattach needs the slot to exist at mount time, and lazy defers it.

Fix: Pick one. Suppress with DJUST_CONFIG = {"suppress_checks": ["A075"]} if you have a deliberate reason.


A090: djust_markdown in use

Severity: Info

What causes it: Templates use {% djust_markdown %}. Message: "{% djust_markdown %} is used in N location(s) (first: <file>:<line>) — djust is rendering Markdown server-side via the Rust pulldown-cmark backend with safe-by-default escaping (ENABLE_HTML never set, javascript: URLs neutralised, 10 MiB input cap)."

Fix: Informational. Suppress with DJUST_CONFIG = {"suppress_checks": ["A090"]}. See Streaming Markdown.


Database Notification Checks (D0xx)

D001: Postgres LISTEN/NOTIFY unavailable

Severity: Warning

What causes it: The default database uses the PostgreSQL backend with the legacy psycopg2 driver, and psycopg (psycopg3) is missing or older than 3.2. Message: "Postgres LISTEN/NOTIFY (db.notifications) is unavailable because psycopg[binary]>=3.2 is not installed." Apps that use @notify_on_save or db.listen() will hit a permanent-failure warning at the first NOTIFY attempt.

Fix: pip install 'psycopg[binary]>=3.2', or silence with SILENCED_SYSTEM_CHECKS = ["djust.D001"] if you don't use db.notifications. See Database Notifications.


Update Notice (U0xx)

U001: Newer djust release or security advisory

Severity: Warning (published advisories affect the installed version) / Info (a newer release exists)

What causes it: With DEBUG=True, the cached update check reports either "SECURITY: djust <version> has N published advisory/advisories (...)" or "djust <latest> is available (you have <installed>)". The system check reads the cache only; it makes no network request.

Fix: Upgrade djust. Disable the notice with DJUST_CONFIG = {"update_check": False} or the DJUST_NO_UPDATE_CHECK=1 environment variable. It is also skipped when CI is set.


Accessibility Checks (Y0xx)

These scan LiveView templates. All are Warnings, so a false positive never fails manage.py check, and each can be suppressed with DJUST_CONFIG['suppress_checks']. See Accessibility.

Y001: Control with no accessible name

Severity: Warning

What causes it: "<file>:<line> -- <tag> has no accessible name (icon-only content and no aria-label)."

Fix: Add aria-label="..." (or aria-labelledby / title) so the control's purpose is announced.


Y002: img without alt

Severity: Warning

What causes it: "<file>:<line> -- <img> tag is missing an 'alt' attribute (WCAG 1.1.1)."

Fix: Use alt="describe the image" for informative images, or alt="" for decorative ones.


Y003: Form control with no label

Severity: Warning

What causes it: "<file>:<line> -- <tag> form control has no associated label (WCAG 1.3.1)." Applies to <input>, <select> and <textarea>; <input> types hidden, submit, button, reset and image are not flagged.

Fix: Associate a label via <label for="...">, wrap the control in a <label>, or add aria-label / aria-labelledby. <label for> matching is per file, so a label in a different template isn't detected.


Y004: Positive tabindex

Severity: Warning

What causes it: '<file>:<line> -- positive tabindex="N" overrides natural focus order (WCAG 2.4.3).'

Fix: Use tabindex="0" to add an element to the natural order, or tabindex="-1" to make it focusable only programmatically.


Audio Checks (djust.audio.*)

These run for routed LiveViews that use AudioMixin. See Audio.

djust.audio.E001: audio_banks value is not a SoundBank

Severity: Error

What causes it: "audio_banks values must be SoundBank instances"

Fix: Make every value in the view's audio_banks a SoundBank.


djust.audio.W001: Declared sound asset not found

Severity: Warning

What causes it: "Declared sound asset not found: <path>". The staticfiles finders can't locate a sound file declared in a bank.

Fix: Add the file to a staticfiles directory before collectstatic.


Theming Checks (djust_theming.*)

These are registered by the djust.theming app under Django's compatibility tag, so they run with a plain manage.py check, not with --tag djust.

djust_theming.E001: theme_context processor missing

Severity: Warning (an Error before 1.2.1; the id keeps its E prefix so existing SILENCED_SYSTEM_CHECKS entries still match)

The processor is optional. {% theme_head %}, {% theme_switcher %} and {% theme_panel %} work without it; it only supplies the {{ theme_head }}-style variables, and it pre-renders the theme chunks on every request that uses a RequestContext. If your templates use only the tags, silence the check instead of adding the processor.

What causes it: "djust.theming.context_processors.theme_context is not in any TEMPLATES backend's context_processors list. Theme template variables (theme_head, theme_switcher, etc.) will not be available; the {% theme_head %} / {% theme_switcher %} tags still work."

Fix: Add "djust.theming.context_processors.theme_context" to TEMPLATES[0]['OPTIONS']['context_processors'] if templates use the {{ theme_head }} variables, or add "djust_theming.E001" to SILENCED_SYSTEM_CHECKS if they use only the tags.


djust_theming.E002: Unknown theme preset

Severity: Error

What causes it: 'LIVEVIEW_CONFIG["theme"]["preset"] is set to "<name>", which is not a registered theme preset.'

Fix: Use one of the preset names listed in the check's hint.


djust_theming.E003: Unknown design system

Severity: Error

What causes it: 'LIVEVIEW_CONFIG["theme"]["theme"] is set to "<name>", which is not a registered design system.'

Fix: Use one of the design system names listed in the check's hint.


djust_theming.E004: Invalid css_prefix

Severity: Error

What causes it: 'css_prefix "<prefix>" contains invalid characters. Only letters, digits, and hyphens are allowed, and it must start with a letter.'

Fix: Use a prefix like "djt-" or "myapp-".


djust_theming.W001: Preset fails WCAG AA contrast

Severity: Warning

What causes it: The active theme preset has a colour pair below the WCAG AA minimum. Message: 'Preset "<name>" <mode> mode: <label> contrast ratio X:1 < Y:1 (WCAG AA)'.

Fix: Adjust the named foreground or background colour to reach at least the minimum ratio.


djust_theming.W002: css_prefix without a trailing hyphen

Severity: Warning

What causes it: 'css_prefix "<prefix>" does not end with "-". Component classes will render as ".<prefix>btn" instead of ".<prefix>-btn".'

Fix: Add a trailing -, e.g. "dj-" instead of "dj".


Permissions Document Findings (P0xx)

These codes come from manage.py djust_audit --permissions permissions.yaml and validate the actual code against a committed declarative permissions document. See Declarative Permissions Document for the full setup guide.

P001: View declared in permissions.yaml but not found in code

Severity: Error

What causes it: An entry in permissions.yaml points at a dotted view path that no longer exists in the codebase. Probably a stale declaration after a view was removed or renamed.

Fix: Remove the stale entry from permissions.yaml, or restore the view if it was deleted by mistake.


P002: View found in code but not declared in permissions.yaml

Severity: Error (strict mode only)

What causes it: A new LiveView was added to the codebase without a corresponding entry in permissions.yaml. Raised unless the document sets strict: false (strict is the default).

Fix: Add the view to permissions.yaml with its intended auth config:

views:
  myapp.views.NewView:
    public: true  # or: login_required: true, permissions: [...]

P003: Document says public but code has auth

Severity: Error

What causes it: permissions.yaml declares public: true for a view but the code sets login_required=True or permission_required=[...]. Either the declaration is out of date or someone added auth to a previously public view without updating the document.

Fix: Reconcile the two sources of truth — either remove the public: true and declare the actual auth config, or revert the code change if the view really should be public.


P004: Document says auth required but code has none

Severity: Error

What causes it: permissions.yaml declares login_required: true or permissions: [...] but the code has neither login_required=True nor permission_required, and there is no custom check_permissions() override or dispatch-based auth mixin.

Fix: Add the intended auth to the view class:

class MyView(LiveView):
    login_required = True
    permission_required = ["myapp.view_something"]

P005: Permission list mismatch

Severity: Error

What causes it: permissions.yaml lists one set of permissions for a view, but the code's permission_required attribute has a different set. Order is not significant; the comparison is set-equality.

Fix: Either update the document (if the code change was intentional) or revert the code (if the document is the source of truth).


P006: object_scoping field not referenced

Severity: Warning (reserved)

What causes it: Reserved — not currently emitted. The code is registered for a future check that object_scoping.fields: [...] declared for a view are referenced in get_object() or equivalent. Today object_scoping.fields is documentation-only.

Fix: Verify manually that your get_object() implementation scopes the query by the declared fields.


P007: Roles declaration (informational)

Severity: Info

What causes it: permissions.yaml lists roles: [...] for a view. djust cannot verify Django group membership at static-analysis time, so this is treated as documentation only. The info-level finding is recorded so reviewers can see which roles are expected.

Fix: Nothing to fix — the finding is informational. If you don't want to see these in output, filter by severity in CI.


Live Runtime Probe Findings (L0xx)

These codes come from manage.py djust_audit --live <url> and reflect actual runtime behavior of a deployed environment. They catch misconfigurations that static analysis cannot see — middleware correctly configured in settings.py but the response is stripped by nginx / ingress / proxy, or a firewall allows what settings appear to deny.

L001: Content-Security-Policy header missing

Severity: Error

What causes it: The live response has no Content-Security-Policy header. Common causes:

  1. csp.middleware.CSPMiddleware is not in MIDDLEWARE in the active settings module.
  2. An ingress / reverse proxy is stripping the header on the way out.
  3. A response middleware runs after CSPMiddleware and removes it.

Fix: Verify the header appears in a dev curl -sI http://localhost:8000/. If it does, the reverse proxy is the culprit — check your nginx/ingress configuration for proxy_hide_header or CloudFront header stripping.


L002: CSP contains 'unsafe-inline'

Severity: Warning

What causes it: script-src or style-src in the CSP header includes 'unsafe-inline', which negates most of CSP's XSS defense.

Fix: Enable nonce-based CSP (see #655 for djust's nonce support) and drop 'unsafe-inline':

# settings.py
CSP_INCLUDE_NONCE_IN = ("script-src", "script-src-elem", "style-src", "style-src-elem")
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'",)

L003: CSP contains 'unsafe-eval'

Severity: Warning

What causes it: script-src includes 'unsafe-eval', allowing dynamic code execution via eval(), new Function(), etc.

Fix: Remove 'unsafe-eval' from CSP_SCRIPT_SRC. If a third-party library requires it, isolate that library in an iframe with a separate CSP.


L004: Strict-Transport-Security header missing

Severity: Error (on HTTPS URLs only)

What causes it: The HTTPS response has no Strict-Transport-Security header. HTTP URLs are exempt from this check.

Fix:

# settings.py
SECURE_HSTS_SECONDS = 31_536_000  # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

L005: HSTS max-age below 1 year

Severity: Warning

What causes it: Strict-Transport-Security: max-age=... is less than 31536000 (1 year). The HSTS preload list requires at least 1 year.

Fix: Set SECURE_HSTS_SECONDS = 31_536_000 (or higher) in settings.py.


L006: HSTS missing 'includeSubDomains'

Severity: Info

What causes it: The HSTS header doesn't include includeSubDomains, so subdomains are not protected.

Fix: SECURE_HSTS_INCLUDE_SUBDOMAINS = True in settings.py.


L007: HSTS missing 'preload'

Severity: Info

What causes it: The HSTS header doesn't include preload, so the domain can't be added to the HSTS preload list.

Fix: SECURE_HSTS_PRELOAD = True in settings.py, then submit to hstspreload.org.


L008: X-Frame-Options header missing

Severity: Error

What causes it: No X-Frame-Options header. Your pages can be framed by any other site, enabling clickjacking.

Fix:

# settings.py
X_FRAME_OPTIONS = "DENY"  # or "SAMEORIGIN"

Make sure django.middleware.clickjacking.XFrameOptionsMiddleware is in MIDDLEWARE.


L009: X-Content-Type-Options missing or wrong value

Severity: Error

What causes it: The response has no X-Content-Type-Options header, or its value is not nosniff. Browsers may MIME-sniff content and render uploaded files as HTML.

Fix: SECURE_CONTENT_TYPE_NOSNIFF = True in settings.py (this is Django's default in modern versions).


L010: Referrer-Policy header missing

Severity: Warning

What causes it: No Referrer-Policy header. The default browser behavior may leak full URLs in the Referer header of outgoing requests.

Fix: SECURE_REFERRER_POLICY = "same-origin" (or "strict-origin-when-cross-origin").


L011: Permissions-Policy header missing

Severity: Warning

What causes it: No Permissions-Policy header. Modern browsers allow the site to restrict access to powerful APIs (camera, microphone, geolocation, payment) — without this header, any same-origin script can request them.

Fix: Add a middleware that emits:

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()

L012: Cross-Origin-Opener-Policy missing

Severity: Info

What causes it: No Cross-Origin-Opener-Policy header. This isolates your window from cross-origin windows that open it (important for protecting against Spectre-class attacks).

Fix: SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin" in settings.py.


L013: Cross-Origin-Resource-Policy missing

Severity: Info

What causes it: No Cross-Origin-Resource-Policy header on the response. Your resources may be embedded by arbitrary cross-origin pages.

Fix: Set via a middleware or reverse proxy header: Cross-Origin-Resource-Policy: same-site.


L014: Server header leaks version

Severity: Warning

What causes it: The Server response header contains something that looks like a version string (nginx/1.25.3, Apache/2.4.58, Python/3.12, uvicorn/0.23, etc.). Attackers can match the version to known CVEs.

Fix: Configure your reverse proxy to suppress the version:

# nginx
server_tokens off;

L015: X-Powered-By header present

Severity: Warning

What causes it: The response includes X-Powered-By, typically revealing the framework/language. Useful information for attackers; not useful for anything else.

Fix: Configure your reverse proxy to strip the header, or remove it at the application layer.


Severity: Error

What causes it: The sessionid (or equivalent) cookie doesn't have the HttpOnly attribute. JavaScript can read it via document.cookie, enabling session theft via XSS.

Fix: SESSION_COOKIE_HTTPONLY = True (Django's default).


Severity: Error (on HTTPS URLs only)

What causes it: On an HTTPS response, the session cookie doesn't have the Secure attribute. The cookie may be sent over plaintext HTTP if the user's connection is downgraded.

Fix: SESSION_COOKIE_SECURE = True in settings.py.


Severity: Warning

What causes it: The session cookie has no SameSite attribute. Modern browsers default to Lax in most cases, but explicit declaration is safer.

Fix: SESSION_COOKIE_SAMESITE = "Lax" (or "Strict").


Severity: Error

What causes it: The csrftoken cookie doesn't have the HttpOnly attribute.

Fix: CSRF_COOKIE_HTTPONLY = True in settings.py.


Severity: Error (on HTTPS URLs only)

What causes it: The CSRF cookie doesn't have Secure on an HTTPS response.

Fix: CSRF_COOKIE_SECURE = True in settings.py.


L040: /.git/config publicly accessible

Severity: Error

What causes it: A GET /.git/config request returns 2xx. The entire git history of your repository is exposed, including potentially-leaked credentials in commits.

Fix: Configure your reverse proxy to deny access to /.git/:

# nginx
location ~ /\.git { deny all; return 404; }

L041: /.env publicly accessible

Severity: Error

What causes it: A GET /.env request returns 2xx. Environment variable files typically contain secrets (database passwords, API keys).

Fix: Never serve .env files from your web root. Store them outside the static root, and configure your reverse proxy to 404 the path.


L042: Django debug toolbar exposed

Severity: Error

What causes it: A GET /__debug__/ request returns 2xx in production. The Django Debug Toolbar is installed and accessible.

Fix: Guard the debug toolbar with DEBUG=True and INTERNAL_IPS:

# settings.py
if DEBUG:
    INSTALLED_APPS += ["debug_toolbar"]
    MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]
    INTERNAL_IPS = ["127.0.0.1"]

L043: /robots.txt not present

Severity: Info

What causes it: /robots.txt returns 404. Not a security issue — an informational finding suggesting you add one to control crawler behavior explicitly.

Fix: Serve a robots.txt at the web root if you want to set crawl rules.


L044: /.well-known/security.txt not present (RFC 9116)

Severity: Info

What causes it: /.well-known/security.txt returns 404. RFC 9116 recommends publishing a security contact file for security researchers.

Fix: Create security.txt at your web root:

Contact: security@example.com
Expires: 2027-01-01T00:00:00Z
Preferred-Languages: en

L060: WebSocket accepted cross-origin handshake (CSWSH)

Severity: Error

What causes it: The runtime probe opened a WebSocket handshake with Origin: https://evil.example and the server accepted it. This is the runtime confirmation of #653 — the static check A001 can see whether the middleware is configured, but only this runtime probe can confirm the handshake is actually rejected end-to-end.

Fix: Wrap the WebSocket router in AllowedHostsOriginValidator (or use DjustMiddlewareStack, which does it by default since 0.4.1). See A001 above.


L061: WebSocket probe skipped (--no-websocket-probe)

Severity: Info

What causes it: The probe was disabled via --no-websocket-probe. Recorded as info so the audit report shows which checks ran and which were skipped.


L062: WebSocket probe skipped (websockets package not installed)

Severity: Info

What causes it: The websockets Python package is not installed, so the CSWSH probe can't run.

Fix: pip install websockets if you want to enable this check. The rest of the runtime probe still runs without it.


L090: Target URL unreachable

Severity: Error

What causes it: urllib couldn't connect to the target — DNS failure, connection refused, timeout, or similar network error.

Fix: Verify the URL is correct, the server is running, and your network can reach it. Check for typos in the hostname or port.


L091: Target URL returned non-2xx status

Severity: Error

What causes it: The target URL returned an HTTP 4xx/5xx status. The probe still inspects the response headers, but the finding is recorded so the report reflects that the main page wasn't loaded successfully.

Fix: Investigate the application error. If the root URL requires auth, pass credentials with --header 'Authorization: Basic ...'.


AST Anti-Pattern Scanner Findings (X0xx)

These findings are emitted by manage.py djust_audit --ast, which walks your Python source and Django templates looking for eight specific security anti-patterns (X001–X008). Every pattern here was either a live vulnerability or a near-miss in the 2026-04-10 pentest of a downstream consumer. The checks are intentionally narrow: false positives are worse than missed findings for a linter that runs on every push.

Suppress a single finding with # djust: noqa X001 on the offending line (or {# djust: noqa X006 #} inside a template). Bare # djust: noqa suppresses every djust.X finding on that line.

Run:

python manage.py djust_audit --ast                              # scan cwd
python manage.py djust_audit --ast --ast-path src/              # scan a directory
python manage.py djust_audit --ast --json                       # machine-readable
python manage.py djust_audit --ast --strict                     # fail on warnings too
python manage.py djust_audit --ast --ast-exclude vendor legacy/ # skip paths
python manage.py djust_audit --ast --ast-no-templates           # .py only

X001: Possible IDOR — object lookup by URL param without auth scoping

Severity: Error

What causes it: A view class whose name ends in DetailView/EditView (or which inherits from LiveView, DetailView, UpdateView, DeleteView, FormView) calls Model.objects.get(pk=self.kwargs["pk"]) without a sibling .filter(owner=request.user) (or user=, tenant=, organization=, team=, created_by=, author=, workspace=) scoping the queryset, and without calling check_permissions or has_perm.

What you see:

ERROR [djust.X001] /path/crm/views.py:42:8 Possible IDOR — object lookup by URL param without auth scoping (ContactDetailView.get_object: add .filter(owner=request.user) or override check_permissions() to scope by owner)

Fix: Scope the queryset by the authenticated user or the tenant the user belongs to:

class ContactDetailView(LiveView):
    def mount(self, request, pk):
        self.contact = Contact.objects.filter(
            owner=request.user
        ).get(pk=pk)

Or override check_permissions() with an explicit membership check and add # djust: noqa X001 on the .get() call if the scoping is implicit (e.g. the tenant filter sits inside a helper manager).


X002: Event handler mutates state without a permission check

Severity: Warning

What causes it: A method decorated with @event_handler contains a call to .create(), .update(), .delete(), .bulk_create(), .bulk_update(), or .save(), and neither of the following is true:

  • The enclosing class sets login_required = True or permission_required = "..." in the class body.
  • The handler itself is wrapped in @permission_required("..."), @login_required, @user_passes_test(...), @staff_member_required, or @superuser_required.

What you see:

WARN [djust.X002] /path/projects/views.py:87:4 Event handler mutates state without a permission check (ProjectView.delete_project: add @permission_required('projects.delete_project'), set login_required=True on the view, or override check_permissions())

Fix: Add class-level auth or a handler decorator:

class ProjectView(LiveView):
    login_required = True
    permission_required = "projects.manage_projects"

    @event_handler
    def delete_project(self, project_id: int = 0, **kwargs):
        Project.objects.filter(pk=project_id).delete()

Or the more granular form:

@permission_required("projects.delete_project")
@event_handler
def delete_project(self, project_id: int = 0, **kwargs):
    Project.objects.filter(pk=project_id).delete()

X003: SQL string formatting in raw()/extra()/execute() — SQLi risk

Severity: Error

What causes it: A call to .raw(...), .extra(...), cursor.execute(...), or cursor.executemany(...) passes a query built with an f-string, a .format() call, or a "..." % ... binary-op. Any of those interpolate Python values directly into the SQL string, bypassing Django's ORM parameter binding.

What you see:

ERROR [djust.X003] /path/reports/queries.py:19:11 SQL string formatting in raw()/extra()/execute() — SQLi risk (.raw(...) with interpolated string)

Fix: Use parametrised queries — pass the values as a second argument and leave %s placeholders in the SQL:

# BAD
Thing.objects.raw(f"SELECT * FROM thing WHERE name = '{name}'")

# GOOD
Thing.objects.raw("SELECT * FROM thing WHERE name = %s", [name])

# GOOD (cursor)
cursor.execute("SELECT * FROM t WHERE name = %s", [name])

X004: Open redirect — request data fed to redirect without is_safe_url

Severity: Error

What causes it: A function contains HttpResponseRedirect(request.GET[...]) (or request.POST, request.GET.get(...), etc.) and the enclosing function does not call url_has_allowed_host_and_scheme or is_safe_url anywhere inside it. The scanner looks for the guard at the same function scope — moving the check into a helper works, but add # djust: noqa X004 on the redirect line so reviewers can see the intent.

What you see:

ERROR [djust.X004] /path/auth/views.py:55:15 Open redirect — request data fed to redirect without is_safe_url (post_login: wrap the target in url_has_allowed_host_and_scheme() before redirecting)

Fix: Validate the URL before redirecting:

from django.utils.http import url_has_allowed_host_and_scheme

def post_login(request):
    target = request.GET.get("next", "/")
    if not url_has_allowed_host_and_scheme(
        target, allowed_hosts={request.get_host()}, require_https=request.is_secure()
    ):
        target = "/"
    return HttpResponseRedirect(target)

X005: mark_safe() with interpolated value — XSS risk

Severity: Error

What causes it: django.utils.safestring.mark_safe(...) or SafeString(...) is called with an argument that is an f-string, a .format() call, or a "..." % ... binary-op. Whatever gets interpolated becomes trusted HTML — if any piece of it is user-controlled, that's stored XSS.

What you see:

ERROR [djust.X005] /path/views.py:72:15 mark_safe() with interpolated value — XSS risk (mark_safe(...) wraps an interpolated string)

Fix: Use django.utils.html.format_html() instead — it behaves like .format() but escapes every substitution:

from django.utils.html import format_html, escape

# BAD
return mark_safe(f"<b>{name}</b>")

# GOOD
return format_html("<b>{}</b>", name)

# ALSO GOOD
return mark_safe(f"<b>{escape(name)}</b>")

X006: Template uses |safe on a view variable

Severity: Warning

What causes it: A .html template contains {{ variable|safe }}. The scanner flags every instance as "worth reviewing" — sometimes |safe is correct (rendering stored HTML that the author wrote in a CMS that already sanitizes it), but it is also the most common XSS foot-gun in Django apps.

What you see:

WARN [djust.X006] /path/templates/blog/post.html:14:4 Template uses |safe on a view variable ({{ body|safe }})

Fix: Remove |safe if the value is user-controlled. If the value is a short snippet you control, wrap it in format_html() on the view side and pass a SafeString instead. Suppress the finding with {# djust: noqa X006 #} on the same line if you have verified the HTML is already sanitised by bleach / nh3.


X007: Template uses {% autoescape off %}

Severity: Warning

What causes it: A .html template opens an {% autoescape off %} block. That disables Django's default auto-escaping for every {{ var }} inside the block — one forgotten |escape turns into stored XSS.

What you see:

WARN [djust.X007] /path/templates/emails/body.html:3:0 Template uses {% autoescape off %}

Fix: Remove the {% autoescape off %} block. If you really need unescaped output (rendering pre-escaped content from a trusted source), prefer an explicit {{ var|safe }} on the one variable that needs it — then X006 documents the exception — instead of a block that silently bypasses escaping for everything inside. Suppress with {# djust: noqa X007 #} on the block opener line if the exception is intentional.


X008: Detail view matches IDOR shape — missing object-permission lifecycle override

Severity: Warning

What causes it: A detail-shaped view class has permission_required, binds a URL-kwarg id to self in mount(), and has event handlers that read it, but neither it nor a base class in the same module overrides has_object_permission() or check_permissions(). View-level permissions don't check that the user may access this particular object.

Fix: Override has_object_permission() (or check_permissions()) to check access to the object. See Authorization for the pattern. Suppress with # djust: noqa X008 if access is scoped elsewhere.


Runtime Errors (DJE-xxx)

These errors appear in server logs during WebSocket communication and VDOM diffing. They are not caught by manage.py check -- they only occur at runtime.

DJE-050: Mixed keyed/unkeyed children

Severity: Warning (performance)

What causes it: A parent element has some children with data-key attributes and some without. This forces the VDOM to use a less efficient diffing strategy.

What you see: A trace-level warning in server logs (visible with DJUST_VDOM_TRACE=1). The page still works but updates may be slower than necessary for large lists.

Fix: Either add data-key to all sibling elements or remove keys from all of them:

<!-- WRONG: mixed keyed/unkeyed -->
<ul>
    <li>Static header</li>
    {% for item in items %}
    <li data-key="{{ item.id }}">{{ item.name }}</li>
    {% endfor %}
</ul>

<!-- CORRECT: all keyed (move static content outside the list) -->
<p>Static header</p>
<ul>
    {% for item in items %}
    <li data-key="{{ item.id }}">{{ item.name }}</li>
    {% endfor %}
</ul>

DJE-051: Duplicate keys

Severity: Warning

What causes it: Two or more sibling elements share the same data-key value. The VDOM engine cannot distinguish between them.

What you see: Trace-level warning in server logs. Elements may be updated incorrectly or not at all.

Fix: Ensure every data-key within the same parent is unique:

<!-- WRONG: duplicate IDs possible if items have same id -->
{% for item in items %}
<li data-key="{{ item.name }}">{{ item.name }}</li>
{% endfor %}

<!-- CORRECT: use unique identifier -->
{% for item in items %}
<li data-key="{{ item.id }}">{{ item.name }}</li>
{% endfor %}

DJE-052: dj-virtual children fell back to index diffing

Severity: Warning

What causes it: A dj-virtual container's children could not be diffed by key (for example, some children lack keys, or keys are duplicated), so the VDOM fell back to index-addressed diffing. Index-addressed ops cannot address items outside the client's visible window.

What you see: A server log warning: DJE-052: a [dj-virtual] container's children ... — falling back to index-addressed diffing, which cannot address items outside the client's visible window. Give every child a unique dj-key.

Fix: Give every child of a dj-virtual container a unique dj-key:

<div dj-virtual="items" dj-virtual-item-height="48"
     style="height: 600px; overflow: auto;">
    {% for item in items %}
    <div dj-key="{{ item.id }}">{{ item.name }}</div>
    {% endfor %}
</div>

Related: Large Lists, List Reordering Performance


DJE-053: Fell back to full HTML update

Severity: Warning

What causes it: An event triggered a re-render, but the VDOM diff returned no patches (or the view forced a full HTML update), after the view already had a rendered baseline. djust then sends the whole view HTML instead of patches. This often means the modified state is rendered outside the dj-root element (e.g., in base.html).

What you see: In server logs:

WARNING [djust] Event 'toggle_sidebar' on DashboardView fell back to full HTML update (DJE-053). Template: myapp/dashboard.html. VDOM diff returned no patches — this may cause event listeners and DOM state to be lost. Debugging steps: ...

djust falls back to replacing the whole view HTML. The page updates, but client-side DOM state and listeners inside the root can be lost.

Common causes:

  1. State rendered outside the VDOM root: The {% if show_panel %} block is in base.html while dj-root is in the child template.
  2. Root not where you expect: dj-root is auto-inferred from dj-view, so check which element carries dj-view; state rendered outside it is not diffed.
  3. Event handler changes state that does not affect the template: The handler updates a variable that is not used in the template.

Fix:

  1. Move the affected template code inside the dj-root element
  2. Use push_event for UI state changes that live outside the VDOM root
  3. Check that dj-root is present on the root element

Related: Template Requirements Guide


Debugging Steps

When you encounter an error, follow this workflow:

1. Run system checks

python manage.py check --tag djust

This catches configuration, validation, security, template, and code quality issues at once.

2. Enable VDOM tracing for runtime issues

DJUST_VDOM_TRACE=1 python manage.py runserver

This produces detailed output for every VDOM diff, including tree structures, patch lists, and performance data. Look for DJE-xxx codes in the output.

3. Check browser DevTools

Open the browser's Network tab and filter by "WS" (WebSocket). You should see:

  • A WebSocket connection to your server
  • Messages flowing back and forth when you click buttons

If there is no WebSocket connection, check C001-C005. If events are sent but no patches come back, check the server log for DJE-053.

4. Check server logs

djust logs warnings for common issues. Look for lines starting with [djust]:

python manage.py runserver 2>&1 | grep "\[djust\]"

5. Run the security audit

For a comprehensive review of authentication and exposed state:

python manage.py djust_audit

Suppressing Checks

You can suppress specific checks on individual lines using # noqa:

# Suppress a specific check
mark_safe(f"<b>{trusted_value}</b>")  # noqa: S001

# Suppress all checks on a line
print("Debug output")  # noqa

# Suppress multiple specific checks
some_code()  # noqa: Q001,S003

See Also