Repository: django/django

Issue:
# Fixed #36940 -- Improved ASGI script prefix path_info handling.

#### Trac ticket number

ticket-36940

#### Branch description

The current `ASGIRequest.__init__` uses `str.removeprefix()` to strip the script name from the request path to compute `path_info`. This is fragile because `removeprefix` is a pure string operation — it doesn't verify that the prefix is a proper path segment boundary.

For example, if `script_name` is `/myapp` and the path is `/myapplication/page`, `removeprefix` would incorrectly produce `lication/page`.

This patch replaces `removeprefix` with a check that ensures the script name is followed by `/` or is the exact path, before stripping it. This addresses the inline TODO comment.

#### AI Assistance Disclosure (REQUIRED)
- [x] **If AI tools were used**, I have disclosed which ones, and fully reviewed and verified their output.

AI tools (Claude) were used to understand the issue and guide the approach. The code was reviewed and verified manually.

#### Checklist
- [x] This PR follows the [contribution guidelines](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/submitting-patches/).
- [x] This PR **does not** disclose a security vulnerability (see [vulnerability reporting](https://docs.djangoproject.com/en/stable/internals/security/)).
- [x] This PR targets the `main` branch.
- [x] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [x] I have checked the "Has patch" ticket flag in the Trac system.
- [ ] I have added or updated relevant tests.
- [ ] I have added or updated relevant docs, including release notes if applicable.
- [ ] I have attached screenshots in both light and dark modes for any UI changes.


Implementation patch:
diff --git a/django/core/handlers/asgi.py b/django/core/handlers/asgi.py
index c8118e1691f9..9555860a7e21 100644
--- a/django/core/handlers/asgi.py
+++ b/django/core/handlers/asgi.py
@@ -54,10 +54,13 @@ def __init__(self, scope, body_file):
         self.path = scope["path"]
         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)
+            script_name = self.script_name.rstrip("/")
+            if self.path.startswith(script_name + "/") or self.path == script_name:
+                self.path_info = self.path[len(script_name) :]
+            else:
+                self.path_info = self.path
         else:
-            self.path_info = scope["path"]
+            self.path_info = self.path
         # HTTP basics.
         self.method = self.scope["method"].upper()
         # Ensure query string is encoded correctly.


Test patch:
diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py
index 83dfd95713b8..625090a66d0c 100644
--- a/tests/handlers/tests.py
+++ b/tests/handlers/tests.py
@@ -346,6 +346,26 @@ def test_force_script_name(self):
         self.assertEqual(request.script_name, "/FORCED_PREFIX")
         self.assertEqual(request.path_info, "/somepath/")
 
+    def test_root_path_prefix_boundary(self):
+        async_request_factory = AsyncRequestFactory()
+        # When path shares a textual prefix with root_path but not at a
+        # segment boundary, path_info should be the full path.
+        request = async_request_factory.request(
+            **{"path": "/rootprefix/somepath/", "root_path": "/root"}
+        )
+        self.assertEqual(request.path, "/rootprefix/somepath/")
+        self.assertEqual(request.script_name, "/root")
+        self.assertEqual(request.path_info, "/rootprefix/somepath/")
+
+    def test_root_path_trailing_slash(self):
+        async_request_factory = AsyncRequestFactory()
+        request = async_request_factory.request(
+            **{"path": "/root/somepath/", "root_path": "/root/"}
+        )
+        self.assertEqual(request.path, "/root/somepath/")
+        self.assertEqual(request.script_name, "/root/")
+        self.assertEqual(request.path_info, "/somepath/")
+
     async def test_sync_streaming(self):
         response = await self.async_client.get("/streaming/")
         self.assertEqual(response.status_code, 200)