Repository: django/channels

Issue:
AuthMiddlewareStack does not work with AsyncHttpConsumers in Channels 3
Hello,

I upgrade Django channels to ver. 3.0.0 and discovered that few features stopped working for me.
This is happening in the manage.py runserver command.
Haven't tried in staging/production yet.

In Django channels ver. 2.4.0, I had the `routing.py` configuration file set up like this:
```
from django.urls import re_path

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.http import AsgiHandler

from apps.logs.ws import consumers as logs_consumers


application = ProtocolTypeRouter({
    "http": AuthMiddlewareStack(
        URLRouter([
            re_path(r"^admin/logs/live/$", logs_consumers.LogsSSEConsumer),
            re_path(r"", AsgiHandler)
        ])
    )
})
```
The `LogsSSEConsumer` consumer class is inheriting from **AsyncHttpConsumer**.

Everything was working on ver. 2.4 and I could access to **self.scope["user"]** in the consumers **handle** method.

Now upgrading to channels ver. 3 I updated `asgi.py` file like this:
```
import os

from django.urls import re_path
from django.core.asgi import get_asgi_application

from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack

from apps.logs.ws import consumers as logs_consumers


os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')

application = ProtocolTypeRouter({
    "http": AuthMiddlewareStack(
        URLRouter([
            re_path(r"^admin/logs/live/$", logs_consumers.LogsSSEConsumer.as_asgi()),
            re_path(r"", get_asgi_application()),
        ])
    ),
})
```
Everything is broken now, a lot of 500 errors showing up in the browser and nothing is working.
In the terminal, there are no errors.

If I remove **AuthMiddlewareStack** wrapper, then everything starts working, except `LogsSSEConsumer` consumer, because i cannot access **self.scope["user"]** anymore.

If I remove **self.scope["user"]** lines from the consumers **handler** method, then everything is working again.

I don't know, am I setting up those routes wrongly or how can I set up now **AsyncHttpConsumer** consumers so I can get access to **self.scope["user"]**.

Same question is, how can i setup **AllowedHostsOriginValidator** wrapper with **AsyncHttpConsumer** consumers?

OS: Docker - python:3.8.6-slim
Dependencies:
Django==3.1.2
channels==3.0.0
channels_redis==3.1.0
django-redis==4.12.1
redis==3.5.3
...




Discussion:
I am able to access `self.scope["user"]` with the following routing:

```
application = ProtocolTypeRouter(
    {
        "websocket": AuthMiddlewareStack(
            URLRouter(
                [
                    re_path(r"^mywebsocketurl/$", MyConsumer.as_asgi()),
                ]
            )
        ),
        "http": get_asgi_application(),
    }
)
```
Python 3.9.0

Dependencies:
Django==3.1.2
channels==3.0.0
channels-redis==3.2.0
aioredis==1.3.1
hiredis==1.1.0

Not installed:
django-redis==4.12.1
redis==3.5.3

Edited: works with Python 3.8.6 too.
> I am able to access `self.scope["user"]` with the following routing:
> 
> ```
> application = ProtocolTypeRouter(
>     {
>         "websocket": AuthMiddlewareStack(
>             URLRouter(
>                 [
>                     re_path(r"^mywebsocketurl/$", MyConsumer.as_asgi()),
>                 ]
>             )
>         ),
>         "http": get_asgi_application(),
>     }
> )
> ```
> 
> Python 3.9.0
> 
> Dependencies:
> Django==3.1.2
> channels==3.0.0
> channels-redis==3.2.0
> aioredis==1.3.1
> hiredis==1.1.0
> 
> Not installed:
> django-redis==4.12.1
> redis==3.5.3
> 
> Edited: works with Python 3.8.6 too.

Hei @ipaleka,

I am using `AsyncHttpConsumer` consumer, which I understand has to be under "http" key in the `ProtocolTypeRouter`.

Tried to put my `LogsSSEConsumer` under the "websocket" key, but then that route is not found.

There's an issue with `SessionMiddleware` in #1551. 

> If I remove AuthMiddlewareStack wrapper, then everything starts working, except LogsSSEConsumer consumer, because i cannot access self.scope["user"] anymore.

This looks as if it's likely related. Let's work on a fix for that. 
Thanks for the report. 

Resolving patch:
diff --git a/channels/sessions.py b/channels/sessions.py
--- a/channels/sessions.py
+++ b/channels/sessions.py
@@ -121,13 +121,10 @@ def delete_cookie(cls, message, key, path="/", domain=None):
         )
 
 
-class SessionMiddleware:
+class InstanceSessionWrapper:
     """
-    Class that adds Django sessions (from HTTP cookies) to the
-    scope. Works with HTTP or WebSocket protocol types (or anything that
-    provides a "headers" entry in the scope).
-
-    Requires the CookieMiddleware to be higher up in the stack.
+    Populates the session in application instance scope, and wraps send to save
+    the session.
     """
 
     # Message types that trigger a session save if it's modified
@@ -136,17 +133,12 @@ class SessionMiddleware:
     # Message types that can carry session cookies back
     cookie_response_message_types = ["http.response.start"]
 
-    def __init__(self, inner):
-        self.inner = inner
+    def __init__(self, scope, send):
         self.cookie_name = settings.SESSION_COOKIE_NAME
         self.session_store = import_module(settings.SESSION_ENGINE).SessionStore
 
-    async def __call__(self, scope, receive, send):
-        """
-        We intercept the send() callable so we can do session saves and
-        add session cookie overrides to send back.
-        """
         self.scope = dict(scope)
+
         if "session" in self.scope:
             # There's already session middleware of some kind above us, pass
             # that through
@@ -162,14 +154,14 @@ async def __call__(self, scope, receive, send):
             self.scope["session"] = LazyObject()
             self.activated = True
 
-        # Resolve the session now we can do it in a blocking way
+        # Override send
+        self.real_send = send
+
+    async def resolve_session(self):
         session_key = self.scope["cookies"].get(self.cookie_name)
         self.scope["session"]._wrapped = await database_sync_to_async(
             self.session_store
         )(session_key)
-        # Override send
-        self.real_send = send
-        return await self.inner(self.scope, receive, self.send)
 
     async def send(self, message):
         """
@@ -238,6 +230,30 @@ def save_session(self):
             )
 
 
+class SessionMiddleware:
+    """
+    Class that adds Django sessions (from HTTP cookies) to the
+    scope. Works with HTTP or WebSocket protocol types (or anything that
+    provides a "headers" entry in the scope).
+
+    Requires the CookieMiddleware to be higher up in the stack.
+    """
+
+    def __init__(self, inner):
+        self.inner = inner
+
+    async def __call__(self, scope, receive, send):
+        """
+        Instantiate a session wrapper for this scope, resolve the session and
+        call the inner application.
+        """
+        wrapper = InstanceSessionWrapper(scope, send)
+
+        await wrapper.resolve_session()
+
+        return await self.inner(wrapper.scope, receive, wrapper.send)
+
+
 # Shortcut to include cookie middleware
 def SessionMiddlewareStack(inner):
     return CookieMiddleware(SessionMiddleware(inner))


Test patch:
diff --git a/tests/test_generic_websocket.py b/tests/test_generic_websocket.py
--- a/tests/test_generic_websocket.py
+++ b/tests/test_generic_websocket.py
@@ -8,6 +8,7 @@
     WebsocketConsumer,
 )
 from channels.layers import get_channel_layer
+from channels.sessions import SessionMiddlewareStack
 from channels.testing import WebsocketCommunicator
 
 
@@ -53,6 +54,47 @@ def disconnect(self, code):
     assert "disconnected" in results
 
 
+@pytest.mark.django_db
+@pytest.mark.asyncio
+async def test_multiple_websocket_consumers_with_sessions():
+    """
+    Tests that multiple consumers use the correct scope when using
+    SessionMiddleware.
+    """
+
+    class TestConsumer(WebsocketConsumer):
+        def connect(self):
+            self.accept()
+
+        def receive(self, text_data=None, bytes_data=None):
+            path = self.scope["path"]
+            self.send(text_data=path)
+
+    app = SessionMiddlewareStack(TestConsumer.as_asgi())
+
+    # Create to communicators.
+    communicator = WebsocketCommunicator(app, "/first/")
+    second_communicator = WebsocketCommunicator(app, "/second/")
+
+    connected, _ = await communicator.connect()
+    assert connected
+    connected, _ = await second_communicator.connect()
+    assert connected
+
+    # Test out of order
+    await second_communicator.send_to(text_data="Echo Path")
+    response = await second_communicator.receive_from()
+    assert response == "/second/"
+
+    await communicator.send_to(text_data="Echo Path")
+    response = await communicator.receive_from()
+    assert response == "/first/"
+
+    # Close out
+    await communicator.disconnect()
+    await second_communicator.disconnect()
+
+
 @pytest.mark.django_db
 @pytest.mark.asyncio
 async def test_websocket_consumer_subprotocol():
diff --git a/tests/test_http.py b/tests/test_http.py
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -391,6 +391,40 @@ async def http_request(self, event):
     assert re.compile(r"Path").search(value) is not None
 
 
+@pytest.mark.django_db(transaction=True)
+@pytest.mark.asyncio
+async def test_muliple_sessions():
+    """
+    Create two application instances and test then out of order to verify that
+    separate scopes are used.
+    """
+
+    async def inner(scope, receive, send):
+        send(scope["path"])
+
+    class SimpleHttpApp(AsyncConsumer):
+        async def http_request(self, event):
+            await database_sync_to_async(self.scope["session"].save)()
+            assert self.scope["method"] == "GET"
+            await self.send(
+                {"type": "http.response.start", "status": 200, "headers": []}
+            )
+            await self.send(
+                {"type": "http.response.body", "body": self.scope["path"].encode()}
+            )
+
+    app = SessionMiddlewareStack(SimpleHttpApp.as_asgi())
+
+    first_communicator = HttpCommunicator(app, "GET", "/first/")
+    second_communicator = HttpCommunicator(app, "GET", "/second/")
+
+    second_response = await second_communicator.get_response()
+    assert second_response["body"] == b"/second/"
+
+    first_response = await first_communicator.get_response()
+    assert first_response["body"] == b"/first/"
+
+
 class MiddlewareTests(unittest.TestCase):
     @pytest.mark.filterwarnings("ignore::DeprecationWarning")
     def test_middleware_caching(self):