ASGIRequest and FORCE_SCRIPT_NAME

Hi all,
Would appreciate if someone could clarify a situation I came across, which may have an obvious reason as to why it was designed as such but I don’t have the requisite experience to understand it.

The issue we faced

We are currently moving from WSGI to ASGI using uvicorn on Django. Locally things were working as they should until we deployed to staging. To cut a longer story short, we weren’t able to login to the Django Admin. The reason being is that we have our backend and frontend hosted separately and use nginx to split traffic. Traffic that has a api/ in the path will be routed to the backend server, otherwise the front end.

In Django there is a FORCE_SCRIPT_NAME setting we used (when we were using WSGI) to set the base path to api/ and all our backend requests took over from there.

The discovery

The WSGIRequest class found in django.core.handlers.wsgi.py is below:

class WSGIRequest(HttpRequest):
    def __init__(self, environ):
        script_name = get_script_name(environ)
        # If PATH_INFO is empty (e.g. accessing the SCRIPT_NAME URL without a
        # trailing slash), operate as if '/' was requested.
        path_info = get_path_info(environ) or "/"
        self.environ = environ
        self.path_info = path_info
        # be careful to only replace the first slash in the path because of
        # http://test/something and http://test//something being different as
        # stated in RFC 3986.
        self.path = "%s/%s" % (script_name.rstrip("/"), path_info.replace("/", "", 1)) # <--- here

Note the self.path = ... line, it appends the script name from FORCE_SCRIPT_NAME to the path on the request object.

This is essential for the Django Admin login as it takes that path and uses it for the login post method.

Now take a look at the ASGIRequest implementation:

class ASGIRequest(HttpRequest):
    """
    Custom request subclass that decodes from an ASGI-standard request dict
    and wraps request body handling.
    """

    # Number of seconds until a Request gives up on trying to read a request
    # body and aborts.
    body_receive_timeout = 60

    def __init__(self, scope, body_file):
        self.scope = scope
        self._post_parse_error = False
        self._read_started = False
        self.resolver_match = None
        self.path = scope["path"]  # <--- here
        self.script_name = get_script_prefix(scope)
        if self.script_name:
            # TODO: Better is-prefix checking, slash handling?
            self.path_info = scope["path"].removeprefix(self.script_name)
        else:
            self.path_info = scope["path"]
        # HTTP basics.
        self.method = self.scope["method"].upper()

No such appending/prefixing happens and because of this, the Django Admin form fails to login, as it doesn’t have the necessary appended api/ that nginx needs to route the request to the backend.

Question
Is this by design or a bug?
If it’s by design, why did it need to differ from WSGI and what was the background reasoning we need to know before we mess with the request.path under ASGI?

Workaround
We created an override def login method on the AdminSite override class for Django Admin that will append the script name to the path on the request.
It works but seems unnecessary.

Thanks for your helps, please let me know if I can provide more context.

See #34394 (ASGIRequest doesn't respect settings.FORCE_SCRIPT_NAME.) – Django — this was resolved in Django 5.0.

It’s worth noting that the ASGI provided path is already the complete value that Django needs, and it’s from that the path_info is calculated (in contrast to WSGI, which has it the other way round.) There’s an historical discussion on the asgiref repo that covers this in detail
.

You should ensure that your nginx configuration is not stripping the path prefix from the request before passing it to Django.

Thank you kindly for your rapid response.

The PR doesn’t touch the path and so won’t fix the Django Admin issue that the ASGI request’s path is used for.
We are on Django 5.2 currently.

Looking at your discussion link it would seem that we would need to use the --root-path cli option for uvicorn in order to ensure the ASGI path gets the appended.
Which effectively renders FORCE_SCRIPT_NAME ineffective in the context of the Django Admin.

My concern is that using --root-path would mess with other routes (non Django Admin) e.g. our REST API endpoints, but I guess that’s something we will need to test thoroughly.

Testing the --root-path with the Django Admin login works currently. Hoping it works for the rest too.

Thanks again

In general SCRIPT_NAME is to come from the application server, so setting --root-path as you say. FORCE_SCRIPT_NAME is meant to override this, if that’s necessary, or to provide a value for e.g. management commands, where a server provided value isn’t available.

1 Like

Cheers, thanks for the guidance on this