SuspiciousFileOperation for static files only under asgi

Hi,
I’ve been happily developing my app under wsgi for several months. Now I need some streaming support so I’ve followed the docs to run under uvicorn. But now nothing works.

If I have django debug toolbar enabled, Django complains that my site logo isn’t in the static dir, which should be fine since the server is in debug mode locally. Commenting out debug toolbar from my installed apps does render the page, but my CSS styles aren’t applied. I get a 404 if I try to load the CSS file.

Again, all this works fine under wsgi. It looks like some kind of static files issue - somehow it seems to not be recognising it’s in debug mode or something. I get 404s for things in the static dir:

INFO:     127.0.0.1:49413 - "GET /static/css/dist/styles.css?v=1732118871 HTTP/1.1" 404 Not Found
[WARNING 2024-11-20 16:07:51,977 django.request:248] Not Found: /static/debug_toolbar/js/timer.js None None
[WARNING 2024-11-20 16:07:51,977 django.request:248] Not Found: /static/debug_toolbar/js/timer.js None None
INFO:     127.0.0.1:49414 - "GET /static/debug_toolbar/js/timer.js HTTP/1.1" 404 Not Found

Here’s a traceback:

Environment:


Request Method: GET
Request URL: http://localhost:8000/content/campaign/203/content/2309/edit

Django Version: 5.1.3
Python Version: 3.11.6
Uvicorn version: 0.32.0
Installed Applications:
['core.apps.CoreConfig',
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.humanize',
 'django.contrib.sites',
 'corsheaders',
 'defender',
 'emailevents',
 'feedback',
 'payments',
 'staticpages',
 'teams',
 'users',
 'allauth',
 'allauth.account',
 'nested_admin',
 'phonenumber_field',
 'formset',
 'djmoney',
 'djstripe',
 'anymail',
 'django_recaptcha',
 'tailwind',
 'theme',
 'template_partials',
 'widget_tweaks',
 'recurring',
 'debug_toolbar',
 'django_browser_reload']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'corsheaders.middleware.CorsMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'defender.middleware.FailedLoginMiddleware',
 'djangopoc.sentry.EnrichEventMiddleware',
 'users.middleware.EnsureMobileVerifiedMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'django_ratelimit.middleware.RatelimitMiddleware',
 'allauth.account.middleware.AccountMiddleware',
 'django.contrib.sites.middleware.CurrentSiteMiddleware',
 'teams.middleware.CurrentProjectMiddleware',
 'django_browser_reload.middleware.BrowserReloadMiddleware',
 'debug_toolbar.middleware.DebugToolbarMiddleware']


Template error:
In template /.../venv/lib/python3.11/site-packages/debug_toolbar/templates/debug_toolbar/panels/staticfiles.html, error at line 30
   The joined path (/images/logo.svg) is located outside of the base path component (/.../core/static)
   20 :   </ol>
   21 : {% else %}
   22 :   <p>{% trans "None" %}</p>
   23 : {% endif %}
   24 : 
   25 : <h4>{% blocktrans count staticfiles|length as staticfiles_count %}Static file{% plural %}Static files{% endblocktrans %}</h4>
   26 : {% if staticfiles %}
   27 :   <dl>
   28 :     {% for staticfile in staticfiles %}
   29 :       <dt><strong><a class="toggleTemplate" href="{{ staticfile.url }}">{{ staticfile }}</a></strong></dt>
   30 :       <dd><samp> {{ staticfile.real_path }} </samp></dd>
   31 :     {% endfor %}
   32 :   </dl>
   33 : {% else %}
   34 :   <p>{% trans "None" %}</p>
   35 : {% endif %}
   36 : 
   37 : 
   38 : {% for finder, payload in staticfiles_finders.items %}
   39 :   <h4>{{ finder }} ({% blocktrans count payload|length as payload_count %}{{ payload_count }} file{% plural %}{{ payload_count }} files{% endblocktrans %})</h4>
   40 :   <table>


Traceback (most recent call last):
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 883, in _resolve_lookup
    current = current[bit]
              ^^^^^^^^^^^^

During handling of the above exception ('StaticFilesPanel' object is not subscriptable), another exception occurred:
  File "/.../venv/lib/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/debug_toolbar/middleware.py", line 97, in __call__
    rendered = toolbar.render_toolbar()
               ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/debug_toolbar/toolbar.py", line 84, in render_toolbar
    return render_to_string("debug_toolbar/base.html", context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/loader.py", line 62, in render_to_string
    return template.render(context, request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/backends/django.py", line 107, in render
    return self.template.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 171, in render
    return self._render(context)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/test/utils.py", line 114, in instrumented_test_render
    return self.nodelist.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1008, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1008, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 969, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/defaulttags.py", line 243, in render
    nodelist.append(node.render_annotated(context))
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 969, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/loader_tags.py", line 210, in render
    return template.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 173, in render
    return self._render(context)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/test/utils.py", line 114, in instrumented_test_render
    return self.nodelist.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1008, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1008, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 969, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/defaulttags.py", line 327, in render
    return nodelist.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1008, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1008, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 969, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/defaulttags.py", line 327, in render
    return nodelist.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1008, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1008, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 969, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1067, in render
    output = self.filter_expression.resolve(context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 718, in resolve
    obj = self.var.resolve(context)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 850, in resolve
    value = self._resolve_lookup(context)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 893, in _resolve_lookup
    current = getattr(current, bit)
              ^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/debug_toolbar/panels/__init__.py", line 103, in content
    return render_to_string(self.template, self.get_stats())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/loader.py", line 62, in render_to_string
    return template.render(context, request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/backends/django.py", line 107, in render
    return self.template.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 171, in render
    return self._render(context)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/test/utils.py", line 114, in instrumented_test_render
    return self.nodelist.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1008, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1008, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 969, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/defaulttags.py", line 327, in render
    return nodelist.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1008, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1008, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 969, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/defaulttags.py", line 243, in render
    nodelist.append(node.render_annotated(context))
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 969, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 1067, in render
    output = self.filter_expression.resolve(context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 718, in resolve
    obj = self.var.resolve(context)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 850, in resolve
    value = self._resolve_lookup(context)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/template/base.py", line 917, in _resolve_lookup
    current = current()
              ^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/debug_toolbar/panels/staticfiles.py", line 25, in real_path
    return finders.find(self.path)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/contrib/staticfiles/finders.py", line 298, in find
    result = finder.find(path, all=all)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/contrib/staticfiles/finders.py", line 203, in find
    match = self.find_in_app(app, path)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/contrib/staticfiles/finders.py", line 216, in find_in_app
    if storage and storage.exists(path):
                   ^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/core/files/storage/filesystem.py", line 206, in exists
    return os.path.lexists(self.path(name))
                           ^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/core/files/storage/filesystem.py", line 220, in path
    return safe_join(self.location, name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../venv/lib/python3.11/site-packages/django/utils/_os.py", line 31, in safe_join
    raise SuspiciousFileOperation(
    ^

Exception Type: SuspiciousFileOperation at /content/campaign/203/content/2309/edit
Exception Value: The joined path (/images/logo.svg) is located outside of the base path component (/.../core/static)

The settings section shows:

DEBUG 	True
...
STATICFILES_DIRS 	

[('node_modules',
  '/.../theme/static_src/node_modules')]

STATICFILES_FINDERS 	

['django.contrib.staticfiles.finders.FileSystemFinder',
 'django.contrib.staticfiles.finders.AppDirectoriesFinder']

STATIC_ROOT 	'/.../static'

STATIC_URL 	'/static/'

STORAGES 	{'default': {'BACKEND': 'storages.backends.s3.S3Storage',
             'OPTIONS': {'bucket_name': 'bucket-media',
                         'file_overwrite': False,
                         'region_name': 'eu-west-1'}},
 'staticfiles': {'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage'}}

The string ‘asgi’ doesn’t appear on the error page at all. I’m just loading a standard (non-async) view.

I’ve also tried running collectstatic but it doesn’t fix the issue.

I’m running uvicorn with: python -m uvicorn djangopoc.asgi:application

Has anyone seen this or know where to start?
Thanks

Your STATIC_ROOT setting looks suspect. Is there meant to be a leading / on it? If you want to point to paths in the parent directory, I’d recommend doing it using BASE_DIR rather than ../, so it uses the correct path for the project, rather than one based on the current working directory.

There were 2 problems:

  1. The static files app doesn’t like leading / characters when run under uvicorn for some reason
  2. More importantly, uvicorn doesn’t automatically serve static files. I needed to explicitly add this to my urls.py:
if settings.DEBUG:
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

Glad you got things sorted!

For serving static fies, static isn’t the most performant way to go, and isn’t really designed for a production-style use-case.

Instead, I’d recommend checking out whitenoise: