Repository: encode/starlette

Issue:
Denying WebSocket connection in WebSocketEndpoint.on_connect leads to Exception
### Discussed in https://github.com/encode/starlette/discussions/1555

<div type='discussions-op-text'>

<sup>Originally posted by **dingensundso** March 27, 2022</sup>
I created a WebSocketEndpoint class in which I want to deny the connection in certain conditions.
When the WebSocket is closed in on_connect I receive the following exception:

```
  File "starlette/endpoints.py", line 83, in dispatch
    close_code = int(message.get("code", status.WS_1000_NORMAL_CLOSURE))
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
```
So code is None when the server denies the connection instead of non-existant.

https://github.com/encode/starlette/blob/e086fc2da361767b532cf690e5203619bbae98aa/starlette/endpoints.py#L72-L87

Changing line 83 to the following should fix the issue:
```python
close_code = int(message.get("code") or status.WS_1000_NORMAL_CLOSURE)               
````
</div>


Discussion:
Can someone confirm this?
I can't reproduce this, a minimal example would be very helpful.
While creating a minimal example I noticed that the problem only exists when using **wsproto** instead of websockets.

Minimal example:
```python
from starlette.applications import Starlette
from starlette.endpoints import WebSocketEndpoint, HTTPEndpoint
from starlette.responses import HTMLResponse
from starlette.routing import Route, WebSocketRoute

html = """
<!DOCTYPE html>
<script>
    var ws = new WebSocket("ws://localhost:8000/ws");
</script>
"""

class Homepage(HTTPEndpoint):
    async def get(self, request):
        return HTMLResponse(html)

class Echo(WebSocketEndpoint):
    async def on_connect(self, websocket):
        await websocket.close()

routes = [
    Route("/", Homepage),
    WebSocketRoute("/ws", Echo)
]

app = Starlette(routes=routes)
```

Running it with `uvicorn ws:app --ws=wsproto` and opening the URL produces the Exception.
As per https://asgi.readthedocs.io/en/latest/specs/www.html#disconnect-receive-event-ws (image below), the application should receive the "code" from the server. There's no default on the specs, meaning that the server MUST send it.

<img width="745" alt="image" src="https://user-images.githubusercontent.com/7353520/161829298-13c9e326-97f9-4a84-b30b-81cfb21e22ad.png">

The interpretation of "if the server" should default to a value, or if the application should be the one to default to, is confusing... That being said, I'll create a fix for Starlette as well.

Resolving patch:
diff --git a/starlette/endpoints.py b/starlette/endpoints.py
--- a/starlette/endpoints.py
+++ b/starlette/endpoints.py
@@ -80,7 +80,9 @@ async def dispatch(self) -> None:
                     data = await self.decode(websocket, message)
                     await self.on_receive(websocket, data)
                 elif message["type"] == "websocket.disconnect":
-                    close_code = int(message.get("code", status.WS_1000_NORMAL_CLOSURE))
+                    close_code = int(
+                        message.get("code") or status.WS_1000_NORMAL_CLOSURE
+                    )
                     break
         except Exception as exc:
             close_code = status.WS_1011_INTERNAL_ERROR


Test patch: