Repository: sanic-org/sanic

Issue:
Sanic WebSockets not working in ASGI mode with daphne as a server
Sanic WebSockets not working in ASGI mode with daphne as a server

Backend code:
```
from sanic import Sanic

app = Sanic("bug")


@app.websocket("/ws/")
async def handler(request, ws):
    await ws.accept()
    while True:
        data = "Hello"
        print("Sending:", data)
        await ws.send(data)
        data = await ws.recv()
        print("Received: ", data)

```
Ran with `daphne -e tcp:80:interface=0.0.0.0 bug:app`

Frontend code:
```
socket = new WebSocket("ws://localhost:80/ws/", []);

socket.onopen = function(e) {
  console.log("Sending to server");
  socket.send("My name is John");
};

socket.onmessage = function(event) {
  console.log(`[message] Data received from server: ${event.data}`);
};

socket.onclose = function(event) {
  if (event.wasClean) {
    console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
  } else {
    // e.g. server process killed or network down
    // event.code is usually 1006 in this case
    console.log('[close] Connection died');
  }
};

socket.onerror = function(error) {
  console.log(`[error] ${error.message}`);
};
ƒ (error) {
  console.log(`[error] ${error.message}`);
}
```

Expected behavior is for the sockets to connect and exchange messages.

Observed behavior is that the backend crashes with `AttributeError: 'WebSocketProtocol' object has no attribute 'handshake_deferred'`. 

Removing the `await ws.accept()` line causes backend to crash with `ValueError: Socket has not been accepted, so cannot send over it`.

Switching the order of send and recv prevents the crash, but no messages are received nor sent.

In all cases frontend disconects with message `[close] Connection died`.


OS: Win10
sanic==20.12.2
daphne==3.0.1



Discussion:
I've reported the same bug in daphne since I can't determine whose fault it is. https://github.com/django/daphne/issues/360
I set up your test as above, eliminating the await and running directly as a sanic app with app.run()

Sanic App:
```
pipenv run python test.py
[2021-03-21 08:03:44 -0500] [4076543] [INFO] Goin' Fast @ http://127.0.0.1:8000
[2021-03-21 08:03:44 -0500] [4076543] [INFO] Starting worker [4076543]
Sending: Hello
Received:  My name is John
Sending: Hello
```

DevTools:
```
Sending to server
2(index):11 [message] Data received from server: Hello
(index):16 [close] Connection closed cleanly, code=1000 reason=
```

I tested with uvicorn and it looks like this may be a problem with the ASGI response
```
[2021-03-21 08:09:51 -0500] [4086237] [ERROR] Exception occurred while handling uri: 'ws://localhost:8000/ws'
Traceback (most recent call last):
  File "/opt/ssadowski/.local/share/virtualenvs/2070-fKWZDJFT/lib/python3.8/site-packages/sanic/app.py", line 729, in handle_request
    response = request.stream.response  # type: ignore
AttributeError: 'ASGIApp' object has no attribute 'response'
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/opt/ssadowski/.local/share/virtualenvs/2070-fKWZDJFT/lib/python3.8/site-packages/sanic/app.py", line 729, in handle_request
    response = request.stream.response  # type: ignore
AttributeError: 'ASGIApp' object has no attribute 'response'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/ssadowski/.local/share/virtualenvs/2070-fKWZDJFT/lib/python3.8/site-packages/uvicorn/protocols/websockets/websockets_impl.py", line 162, in run_asgi
    result = await self.app(self.scope, self.asgi_receive, self.asgi_send)
  File "/opt/ssadowski/.local/share/virtualenvs/2070-fKWZDJFT/lib/python3.8/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "/opt/ssadowski/.local/share/virtualenvs/2070-fKWZDJFT/lib/python3.8/site-packages/sanic/app.py", line 1214, in __call__
    await asgi_app()
  File "/opt/ssadowski/.local/share/virtualenvs/2070-fKWZDJFT/lib/python3.8/site-packages/sanic/asgi.py", line 209, in __call__
    await self.sanic_app.handle_request(self.request)
  File "/opt/ssadowski/.local/share/virtualenvs/2070-fKWZDJFT/lib/python3.8/site-packages/sanic/app.py", line 748, in handle_request
    await self.handle_exception(request, e)
  File "/opt/ssadowski/.local/share/virtualenvs/2070-fKWZDJFT/lib/python3.8/site-packages/sanic/app.py", line 655, in handle_exception
    await response.send(end_stream=True)
  File "/opt/ssadowski/.local/share/virtualenvs/2070-fKWZDJFT/lib/python3.8/site-packages/sanic/response.py", line 122, in send
    await self.stream.send(data, end_stream=end_stream)
  File "/opt/ssadowski/.local/share/virtualenvs/2070-fKWZDJFT/lib/python3.8/site-packages/sanic/asgi.py", line 185, in send
    await self.transport.send(
  File "/opt/ssadowski/.local/share/virtualenvs/2070-fKWZDJFT/lib/python3.8/site-packages/sanic/models/asgi.py", line 92, in send
    await self._send(data)
  File "/opt/ssadowski/.local/share/virtualenvs/2070-fKWZDJFT/lib/python3.8/site-packages/uvicorn/protocols/websockets/websockets_impl.py", line 235, in asgi_send
    raise RuntimeError(msg % message_type)
RuntimeError: Expected ASGI message 'websocket.send' or 'websocket.close', but got 'http.response.start'.
AttributeError: 'ASGIApp' object has no attribute 'response'
```


@sjsadowski This might be an issue with 21.3, but the original ticket says 20.12 so we might have two different things going on here.

@sim1234 Out of curiosity, have you tried the same with `uvicorn` or `hypercorn`?
above was 21.3, below is 20.12.3 w/ uvicorn:

sanic:
```
$ pipenv run uvicorn --host 127.0.0.1 --port 8000 test:app
INFO:     Started server process [4132344]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     ('127.0.0.1', 57078) - "WebSocket /ws" [accepted]
Sending: Hello
Received:  None
Sending: Hello
```
browser:
```
WebSocket connection to 'ws://localhost:8000/ws' failed: 
(anonymous) @ (index):3
(index):25 [error] undefined
(index):20 [close] Connection died
```



Hi!

I tested uvicorn, hypercorn and daphne. 

# environments

1. hardware:
	
    MacBook Pro (13-inch, M1, 2020) 

2. Python:
	
    3.9

3. PythonPackage:
	- Sanic==21.3.2
	- sanic-routing==0.4.0
	- uvicorn==0.13.4
	- Hypercorn==0.11.2
	- daphne==3.0.1
	- websockets==8.1
	- wsproto==1.0.0

# before test

1. shut subprotocol down in webockets

	seems in asgi mode, websockets `subprotocol` is not well-supported. So I did a little change before test.

	```python
	# sanic.websocket.WebSocketConnection

	    async def accept(self) -> None:
	        await self._send(
	            {
	                "type": "websocket.accept",
	                # "subprotocol": ",".join(list(self.subprotocols)),
	            }
	        )
	```

	`subprotocol` causes broken in `uvicorn + wsproto` and `hypercorn` and `dephne`.


2. Comment out the following code

    ```python
    # app.py line 726
                if response:
                    response = await request.respond(response)
                # else:
                #     response = request.stream.response  # type: ignore
    ```

    When sending a websocket request. a exception raised: `AttributeError: 'ASGIApp' object has no attribute 'response'`. looks like `request.stream == sanic.asgi.ASGIApp()` in ASGI mode, AGSI object doesn't have response attribute. 

    these code may cause exceptions. 

# conclusion

## uvicorn 

uvicorn uses webcokets and wsproto as it's implements of websocket protocol, So:

1. uvicorn + websockets: Broken, can't establish websocket connection. (didn't find any solution yet, uvicorn can't get along this version of websockets). 👎 
   
2. uvicorn + wsproto: works fine. 👍 

## dephne

1. All routes are broken(both http and ws) 👎 

    traceback shown:

    ```sh
    File ".../python3.9/site-packages/sanic_routing/router.py", line 63, in resolve
        route, param_basket = self.find_route(
    TypeError: 'NoneType' object is not callable
    ```

    Seems `sanic-routeing` `finalize()` is never called. after set a breakpoint in `Sanic.__call__()`, after call `finalize()` manually in pdb, router works.

    should check signals event when using dephne.

## hypercorn

1. works fine.👍 

@ZinkLu This is awesome work. Thanks you so much.
Glad to help. If there is anything I can PR, let me know.

Resolving patch:
diff --git a/sanic/app.py b/sanic/app.py
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -585,7 +585,12 @@ def url_for(self, view_name: str, **kwargs):
             # determine if the parameter supplied by the caller
             # passes the test in the URL
             if param_info.pattern:
-                passes_pattern = param_info.pattern.match(supplied_param)
+                pattern = (
+                    param_info.pattern[1]
+                    if isinstance(param_info.pattern, tuple)
+                    else param_info.pattern
+                )
+                passes_pattern = pattern.match(supplied_param)
                 if not passes_pattern:
                     if param_info.cast != str:
                         msg = (
@@ -593,13 +598,13 @@ def url_for(self, view_name: str, **kwargs):
                             f"for parameter `{param_info.name}` does "
                             "not match pattern for type "
                             f"`{param_info.cast.__name__}`: "
-                            f"{param_info.pattern.pattern}"
+                            f"{pattern.pattern}"
                         )
                     else:
                         msg = (
                             f'Value "{supplied_param}" for parameter '
                             f"`{param_info.name}` does not satisfy "
-                            f"pattern {param_info.pattern.pattern}"
+                            f"pattern {pattern.pattern}"
                         )
                     raise URLBuildError(msg)
 
@@ -740,17 +745,14 @@ async def handle_request(self, request: Request):
 
             if response:
                 response = await request.respond(response)
-            else:
+            elif not hasattr(handler, "is_websocket"):
                 response = request.stream.response  # type: ignore
-            # Make sure that response is finished / run StreamingHTTP callback
 
+            # Make sure that response is finished / run StreamingHTTP callback
             if isinstance(response, BaseHTTPResponse):
                 await response.send(end_stream=True)
             else:
-                try:
-                    # Fastest method for checking if the property exists
-                    handler.is_websocket  # type: ignore
-                except AttributeError:
+                if not hasattr(handler, "is_websocket"):
                     raise ServerError(
                         f"Invalid response type {response!r} "
                         "(need HTTPResponse)"
@@ -777,6 +779,7 @@ async def _websocket_handler(
 
         if self.asgi:
             ws = request.transport.get_websocket_connection()
+            await ws.accept(subprotocols)
         else:
             protocol = request.transport.get_protocol()
             protocol.app = self
diff --git a/sanic/asgi.py b/sanic/asgi.py
--- a/sanic/asgi.py
+++ b/sanic/asgi.py
@@ -140,7 +140,6 @@ async def create(
                 instance.ws = instance.transport.create_websocket_connection(
                     send, receive
                 )
-                await instance.ws.accept()
             else:
                 raise ServerError("Received unknown ASGI scope")
 
diff --git a/sanic/websocket.py b/sanic/websocket.py
--- a/sanic/websocket.py
+++ b/sanic/websocket.py
@@ -41,7 +41,7 @@ def __init__(
         websocket_write_limit=2 ** 16,
         websocket_ping_interval=20,
         websocket_ping_timeout=20,
-        **kwargs
+        **kwargs,
     ):
         super().__init__(*args, **kwargs)
         self.websocket = None
@@ -154,7 +154,7 @@ def __init__(
     ) -> None:
         self._send = send
         self._receive = receive
-        self.subprotocols = subprotocols or []
+        self._subprotocols = subprotocols or []
 
     async def send(self, data: Union[str, bytes], *args, **kwargs) -> None:
         message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"}
@@ -178,13 +178,28 @@ async def recv(self, *args, **kwargs) -> Optional[str]:
 
     receive = recv
 
-    async def accept(self) -> None:
+    async def accept(self, subprotocols: Optional[List[str]] = None) -> None:
+        subprotocol = None
+        if subprotocols:
+            for subp in subprotocols:
+                if subp in self.subprotocols:
+                    subprotocol = subp
+                    break
+
         await self._send(
             {
                 "type": "websocket.accept",
-                "subprotocol": ",".join(list(self.subprotocols)),
+                "subprotocol": subprotocol,
             }
         )
 
     async def close(self) -> None:
         pass
+
+    @property
+    def subprotocols(self):
+        return self._subprotocols
+
+    @subprotocols.setter
+    def subprotocols(self, subprotocols: Optional[List[str]] = None):
+        self._subprotocols = subprotocols or []


Test patch:
diff --git a/tests/test_asgi.py b/tests/test_asgi.py
--- a/tests/test_asgi.py
+++ b/tests/test_asgi.py
@@ -218,7 +218,7 @@ async def test_websocket_accept_with_no_subprotocols(
 
     message = message_stack.popleft()
     assert message["type"] == "websocket.accept"
-    assert message["subprotocol"] == ""
+    assert message["subprotocol"] is None
     assert "bytes" not in message
 
 
@@ -227,7 +227,7 @@ async def test_websocket_accept_with_subprotocol(send, receive, message_stack):
     subprotocols = ["graphql-ws"]
 
     ws = WebSocketConnection(send, receive, subprotocols)
-    await ws.accept()
+    await ws.accept(subprotocols)
 
     assert len(message_stack) == 1
 
@@ -244,13 +244,13 @@ async def test_websocket_accept_with_multiple_subprotocols(
     subprotocols = ["graphql-ws", "hello", "world"]
 
     ws = WebSocketConnection(send, receive, subprotocols)
-    await ws.accept()
+    await ws.accept(["hello", "world"])
 
     assert len(message_stack) == 1
 
     message = message_stack.popleft()
     assert message["type"] == "websocket.accept"
-    assert message["subprotocol"] == "graphql-ws,hello,world"
+    assert message["subprotocol"] == "hello"
     assert "bytes" not in message