Opened 9 days ago

Closed 9 days ago

Last modified 3 hours ago

#35985 closed Cleanup/optimization (wontfix)

FORCE_SCRIPT_NAME ignored when running reverse() on non-main thread

Reported by: Pēteris Caune Owned by:
Component: Core (URLs) Version: dev
Severity: Normal Keywords:
Cc: Florian Apolloner Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Natalia Bidart)

I've configured my Django project to run on a subpath (under example.org/some_prefix instead of example.org).

The project has a management command which generates URLs using django.urls.reverse().

Since the management command cannot read SCRIPT_NAME from WSGI parameters, the project has FORCE_SCRIPT_NAME = "/some_prefix" in settings.py.

The management command generates URLs that include the prefix as expected if the code runs on main thread. But if the management command spawns a thread, the code running on thread generates URLs without the prefix.

I'm not sure but I think this is related to django.urls.base._prefixes being a Local object. I'm guessing it, as the name suggests, does not share data between threads. Even though set_script_prefix is called on main thread, the other threads do not see it.

A simple workaround is for the user to call set_script_prefix by themselves:

from django.conf import settings
from django.urls import set_script_prefix

def this_will_be_run_on_thread():
    if settings.FORCE_SCRIPT_NAME:
        set_script_prefix(settings.FORCE_SCRIPT_NAME)
    # do work here    

But perhaps there's something Django could do here as well:

  • perhaps, change django.urls implementation so that threads do share the script prefix storage?
  • if there are disadvantages to that, mention this gotcha in the documentation

I'm happy to provide a dummy project demonstrating the issue if that would be helpful.

Change History (5)

comment:1 by Natalia Bidart, 9 days ago

Cc: Florian Apolloner added
Resolution: wontfix
Status: newclosed
Type: BugCleanup/optimization
Version: 5.1dev

Hello Pēteris Caune, thank you for taking the time to create this report. I have read your description carefully and I yes, I agree, the fact that _prefixes is local to the thread means that spawned threads will not share its contents. Also the function set_script_prefix has this docstring:

Set the script prefix for the current thread.

For a workaround, Django will set the prefix when setup is called, perhaps the best option for your management command is to call setup in each thread? Feels cleaner and more correct.

Regarding your comment for change django.urls implementation so that threads do share the script prefix storage, in my opinion this is beyond to what the main goal of Django is. As shown above, this is simple to add/solve to your code base, and to me this is a very specific need arising from a niche use case. I don't think this applies to the broader ecosystem, and Django is a framework designed to offer robust and accurate solutions for common scenarios.

Given the above, I'll close the ticket accordingly, but if you disagree, you can consider starting a new conversation on the Django Forum, where you'll reach a wider audience and likely get extra feedback.

comment:2 by Natalia Bidart, 9 days ago

Description: modified (diff)
Summary: SCRIPT_NAME / FORCE_SCRIPT_NAME ignored when running reverse() on non-main threadFORCE_SCRIPT_NAME ignored when running reverse() on non-main thread

comment:3 by Pēteris Caune, 9 days ago

Thanks for looking into it, Natalia!

I agree, this is a niche use case (in fact, two niche use cases combined – serving app on subpath, and using threads in a management command), and so perhaps not worth the effort and risk of changing django.urls.base._prefixes.

For a workaround, Django will set the prefix when setup is called, perhaps the best option for your management command is to call setup in each thread?

I'm not sure – I'm using threads in management commands with no specific per-thead initialization currently, and the threads are able to access Django settings, models, etc. I suspect calling django.setup() in each thread would do duplicate work and initialize stuff that is already initialized.


How about documenting set_script_prefix()? In https://docs.djangoproject.com/en/5.1/ref/urlresolvers/#get-script-prefix there is a section for get_script_prefix but not for set_script_prefix. I cannot promise any quality, but I'm happy to take a first stab at it.

comment:4 by Natalia Bidart, 9 days ago

Thank you for your understanding and your willingness to help.

My understanding of the code and docs is that set_script_prefix may be intentionally undocumented. Otherwise, Django needs to guarantee its API stability and documented behavior, and any change has to go thru the documented deprecation process. Because of this, I don't believe that documenting it would be a desirable change.

comment:5 by Florian Apolloner, 3 hours ago

Hi Pēteris, nice to see you here again :) I will go out on a limb and say that I think FORCE_SCRIPT_NAME and everything it interacts with is broken beyond repair. I had similar problems in a project of mine and I opted for the following:

prefix = f"{settings.BASE_URL.lstrip('/')}/" if settings.BASE_URL else ""

urlpatterns = [
    path(f"{prefix}api/v1/", api.urls),
    path(f"{prefix}", include(oversight_urlpatterns)),
    path(f"{prefix}accounts/", include("allauth.urls")),
]

So essentially I introduced a BASE_URL which I prepend so reverse works fine. Given how your WSGI integration works you might run into problems though with the WSGI environment. That is usually fixable (in my case it was not a problem since I am proxying from traefik to gunicorn -- if you are using something more embedded like mod_wsgi/uwsgi then you might need some adjustments). All in all my approach has additional benefits: I can reuse the BASE_URL for MEDIA_URL/STATIC_URL as well without having to worry that it breaks easily like it does with FORCE_SCRIPT_NAME and it will also work with threads ;) If you have more questions about that approach feel free to contact me out of band or mention me in a healthchecks.io (I assume it is for said project?) issue/discussion.

Like Natalia I am kinda hesitant to document set_script_prefix. In an ideal world I'd love Django to adopt a BASE_URL (which actually is the whole URL and not just the path like in my project) because then Django finally knows where it is mounted and we could extend url resolving to handle subdomains etc as well. Also most of the ALLOWED_HOSTS magic becomes moot if you just generate URLs etc using BASE_URL without having to rely on the host header.

Regarding running django.setup() in multiple threads: I don't think that this is something we support or test -- so I would stay far away from it.

Note: See TracTickets for help on using tickets.
Back to Top