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.