﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
36653	FORCE_SCRIPT_NAME is not respected for static URLs	Brian Helba		"The documentation for the `STATIC_URL` setting states:
> If `STATIC_URL` is a relative path, then it will be prefixed by the server-provided value of `SCRIPT_NAME` (or / if not set). This makes it easier to serve a Django application in a subpath without adding an extra configuration to the settings.

However, when using `FORCE_SCRIPT_NAME`, the value of `STATIC_URL` when serving requests is never updated appropriately. This breaks URL construction via `django.templatetags.static.static` (used in templates as `{% static ... %}`).

For example, this causes the Django Admin pages to break when using `FORCE_SCRIPT_NAME` to serve Django under a subpath. Generally, using `FORCE_SCRIPT_NAME` causes Django to behave incorrectly: view URLs are constructed to respect it, static URLs are constructed to not respect it.

I believe that this bug likely also affects static URLs when using the `SCRIPT_NAME` WSGI environment variable too, but I haven't verified that yet.

----
There is definitely some history here, but I believe previous bug reports have struggled to articulate this problem.

Long ago, the following reports were made, which I believe are ''not'' relevant to this problem (but I'm summarizing them because they've been claimed to be duplicates of this problem):
* #7930 only discussed view URLs (and using `reverse()`); this now works as expected, but static URLs are still broken
* #30634 is probably irrelevant; it claimed vague problems with `SCRIPT_NAME` and runserver; the problem here is general to all servers, including both runserver and WSGI
* #31724 is probably a true duplicate of #7930 

More recently, we've seen:
* #34892 is probably the same as this problem, but the reporter struggled to articulate the behavior and I believe it was mistakenly closed as a duplicate of the aforementioned old issues
* #35985 claimed that this is problem is limited to using threading within management commands, then got derailed by the niche use case and suggestions around the low-level `set_script_prefix` API; it does contain the very useful suggestion to invoke `django.setup()` in each thread, which I don't believe was adequately explored

---
I believe that I understand the exact cause of the bug. Note, my use of some lifecycle events and specific thread names may be limited to the runserver case, but the exact same behaviors manifest with WSGI (and I believe that equivalent things are occurring with a multiprocess lifecycle).

1. `django.setup()` is [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/core/management/__init__.py#L395 called very early by runserver]
2. `django.setup()` [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/__init__.py#L20-L23 calls `set_script_prefix`]
3. `set_script_prefix` [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/urls/base.py#L120C5-L126C20 correctly sets] `django.urls.base._prefixes.value`, but only for the current thread (since `_prefixes` is a thread-local object)
4. a new thread, `django-main-thread`, [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/utils/autoreload.py#L666-L670 is spawned]; again, I believe (and can locate if necessary) that an equivalent event also happens in a WSGI lifecycle
5. `check_url_settings` [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/core/checks/urls.py#L108-L109 runs and accesses `settings.STATIC_URL`]; importantly, this is the first time in the startup lifecycle that `settings.STATIC_URL` has ever been accessed
6. `django.conf.__getattr__` [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/conf/__init__.py#L82-L83 has special logic for `STATIC_URL`], so it calls the staticmethod `LazySettings._add_script_prefix`
7. `LazySettings._add_script_prefix` [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/conf/__init__.py#L134 calls `get_script_prefix`]
8. `get_script_prefix` [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/urls/base.py#L129-L135 looks at `django.urls.base._prefixes.value`], but it's running in a new thread (step 4), so it doesn't contain the `FORCE_SCRIPT_NAME` value (step 3) and returns `""/""`
9. `django.conf.__getattr__` (step 6) permanently caches the incorrect value of `settings.STATIC_URL` in `LazySettings.__dict__` (which is not thread-local); all future requests for `settings.STATIC_URL` will receive the incorrect value (`""/""`, instead of `settings.FORCE_SCRIPT_NAME`)
10. The first HTTP request comes in
11. `WSGIHandler.__call__` [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/core/handlers/wsgi.py#L121 calls] `get_script_name`
12. `get_script_name` [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/core/handlers/wsgi.py#L162-L163 correctly returns] `settings.FORCE_SCRIPT_NAME`
13. `WSGIHandler.__call__` [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/core/handlers/wsgi.py#L121 calls] `set_script_prefix` with the correct argument (the value `settings.FORCE_SCRIPT_NAME`)
14. `set_script_prefix` (just as in step 3) [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/urls/base.py#L120C5-L126C20 finally sets] `django.urls.base._prefixes.value` (which, remember, is a thread-local variable, but will persist for at least the remainder of this HTTP request) with the correct value
15. While rendering the HTTP response, a template or some code calls [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/templatetags/static.py#L174-L179 `django.templatetags.static`]; assume that the app `""django.contrib.staticfiles""` is installed and configured to use some subclass of `StaticFilesStorage` (which is Django's typical configuration)
16. `django.contrib.staticfiles.storage.staticfiles_storage.url()` [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/templatetags/static.py#L129 is called]
17. `staticfiles_storage` (an instance of `StaticFilesStorage`) [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/core/files/storage/handler.py#L46 is lazily constructed]
18. `StaticFilesStorage.__init__` is called; assume it has no arguments from `settings.STORAGES[""staticfiles""][""OPTIONS""] ([https://docs.djangoproject.com/en/5.2/ref/settings/#storages this is Django's default])
19. `StaticFilesStorage.__init__` [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/contrib/staticfiles/storage.py#L27-L30 defaults to set its `self._base_url`] to `settings.STATIC_URL`, but `settings.STATIC_URL` returns an incorrect value (step 9)
20. Continuing the call in step 16, `FilesystemStorage.url` [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/core/files/storage/filesystem.py#L212 forms the actual URL] from `self.base_url`
21. `self.base_url`, [https://github.com/django/django/blob/1167cd1d639c3fee69dbdef351d31e8a17d1fedf/django/core/files/storage/filesystem.py#L47-L51 a cached property], relies on the incorrect `self._base_url`
22. A static file URL is returned with the incorrect base URL, not respecting `settings.FORCE_SCRIPT_NAME`
23. Subsequent HTTP requests skip steps 17-19, but otherwise reply the work from step 10 onwards (and also result in incorrect static URLs)

In summary, the problem is that although there's code to attempt to set (via `set_script_prefix`) the correct script name both on Django's startup and again on every individual HTTP request, `settings.STATIC_URL` slips between a lifecycle ""crack"" and ends up with the wrong value (which doesn't incorporate the script name, ''contrary to its documentation''), which persists over the entire request-response lifecycle."	Bug	new	contrib.staticfiles	5.2	Normal			Brian Helba	Unreviewed	0	0	0	0	0	0
