Repository: aio-libs/aiohttp

Issue:
Missing close status codes for websockets
🐞 **Describe the bug**

ref: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
current: https://github.com/aio-libs/aiohttp/blob/707736b/aiohttp/http_websocket.py#L31-L42

The close code 1014, 1015 is missing, and one of my use case (websocket proxy) involves 1014,
which results in an unexpected error "Invalid close code: 1014".

I think we need to update the list.


Discussion:
@achimnol do you mean that the values are missing in the `WSCloseCode` enum?
There is an interesting question: how to prevent such problem in future, when a new code will be added again?
It could be a GitHub  action that periodically fetchs the https://github.com/mdn/content/blob/main/files/en-us/web/api/closeevent/index.html , parses it and compares to the enum in master branch. In case of mismatch creates an issue with a tag.

Resolving patch:
diff --git a/aiohttp/client_ws.py b/aiohttp/client_ws.py
--- a/aiohttp/client_ws.py
+++ b/aiohttp/client_ws.py
@@ -13,6 +13,7 @@
     WS_CLOSED_MESSAGE,
     WS_CLOSING_MESSAGE,
     WebSocketError,
+    WSCloseCode,
     WSMessage,
     WSMsgType,
 )
@@ -109,7 +110,7 @@ def _send_heartbeat(self) -> None:
     def _pong_not_received(self) -> None:
         if not self._closed:
             self._closed = True
-            self._close_code = 1006
+            self._close_code = WSCloseCode.ABNORMAL_CLOSURE
             self._exception = asyncio.TimeoutError()
             self._response.close()
 
@@ -171,7 +172,7 @@ async def send_json(
     ) -> None:
         await self.send_str(dumps(data), compress=compress)
 
-    async def close(self, *, code: int = 1000, message: bytes = b"") -> bool:
+    async def close(self, *, code: int = WSCloseCode.OK, message: bytes = b"") -> bool:
         # we need to break `receive()` cycle first,
         # `close()` may be called from different task
         if self._waiting is not None and not self._closed:
@@ -184,11 +185,11 @@ async def close(self, *, code: int = 1000, message: bytes = b"") -> bool:
             try:
                 await self._writer.close(code, message)
             except asyncio.CancelledError:
-                self._close_code = 1006
+                self._close_code = WSCloseCode.ABNORMAL_CLOSURE
                 self._response.close()
                 raise
             except Exception as exc:
-                self._close_code = 1006
+                self._close_code = WSCloseCode.ABNORMAL_CLOSURE
                 self._exception = exc
                 self._response.close()
                 return True
@@ -202,11 +203,11 @@ async def close(self, *, code: int = 1000, message: bytes = b"") -> bool:
                     async with async_timeout.timeout(self._timeout.ws_close):
                         msg = await self._reader.read()
                 except asyncio.CancelledError:
-                    self._close_code = 1006
+                    self._close_code = WSCloseCode.ABNORMAL_CLOSURE
                     self._response.close()
                     raise
                 except Exception as exc:
-                    self._close_code = 1006
+                    self._close_code = WSCloseCode.ABNORMAL_CLOSURE
                     self._exception = exc
                     self._response.close()
                     return True
@@ -242,15 +243,15 @@ async def receive(self, timeout: Optional[float] = None) -> WSMessage:
                     self._waiting = None
                     set_result(waiter, True)
             except (asyncio.CancelledError, asyncio.TimeoutError):
-                self._close_code = 1006
+                self._close_code = WSCloseCode.ABNORMAL_CLOSURE
                 raise
             except EofStream:
-                self._close_code = 1000
+                self._close_code = WSCloseCode.OK
                 await self.close()
                 return WSMessage(WSMsgType.CLOSED, None, None)
             except ClientError:
                 self._closed = True
-                self._close_code = 1006
+                self._close_code = WSCloseCode.ABNORMAL_CLOSURE
                 return WS_CLOSED_MESSAGE
             except WebSocketError as exc:
                 self._close_code = exc.code
@@ -259,7 +260,7 @@ async def receive(self, timeout: Optional[float] = None) -> WSMessage:
             except Exception as exc:
                 self._exception = exc
                 self._closing = True
-                self._close_code = 1006
+                self._close_code = WSCloseCode.ABNORMAL_CLOSURE
                 await self.close()
                 return WSMessage(WSMsgType.ERROR, exc, None)
 
diff --git a/aiohttp/http_websocket.py b/aiohttp/http_websocket.py
--- a/aiohttp/http_websocket.py
+++ b/aiohttp/http_websocket.py
@@ -33,6 +33,7 @@ class WSCloseCode(IntEnum):
     GOING_AWAY = 1001
     PROTOCOL_ERROR = 1002
     UNSUPPORTED_DATA = 1003
+    ABNORMAL_CLOSURE = 1006
     INVALID_TEXT = 1007
     POLICY_VIOLATION = 1008
     MESSAGE_TOO_BIG = 1009
@@ -40,6 +41,7 @@ class WSCloseCode(IntEnum):
     INTERNAL_ERROR = 1011
     SERVICE_RESTART = 1012
     TRY_AGAIN_LATER = 1013
+    BAD_GATEWAY = 1014
 
 
 ALLOWED_CLOSE_CODES = {int(i) for i in WSCloseCode}
diff --git a/aiohttp/web_ws.py b/aiohttp/web_ws.py
--- a/aiohttp/web_ws.py
+++ b/aiohttp/web_ws.py
@@ -19,6 +19,7 @@
     WebSocketError,
     WebSocketReader,
     WebSocketWriter,
+    WSCloseCode,
     WSMessage,
     WSMsgType as WSMsgType,
     ws_ext_gen,
@@ -144,7 +145,7 @@ def _send_heartbeat(self) -> None:
     def _pong_not_received(self) -> None:
         if self._req is not None and self._req.transport is not None:
             self._closed = True
-            self._close_code = 1006
+            self._close_code = WSCloseCode.ABNORMAL_CLOSURE
             self._exception = asyncio.TimeoutError()
             self._req.transport.close()
 
@@ -346,7 +347,7 @@ async def write_eof(self) -> None:  # type: ignore
         await self.close()
         self._eof_sent = True
 
-    async def close(self, *, code: int = 1000, message: bytes = b"") -> bool:
+    async def close(self, *, code: int = WSCloseCode.OK, message: bytes = b"") -> bool:
         if self._writer is None:
             raise RuntimeError("Call .prepare() first")
 
@@ -368,10 +369,10 @@ async def close(self, *, code: int = 1000, message: bytes = b"") -> bool:
                 assert writer is not None
                 await writer.drain()
             except (asyncio.CancelledError, asyncio.TimeoutError):
-                self._close_code = 1006
+                self._close_code = WSCloseCode.ABNORMAL_CLOSURE
                 raise
             except Exception as exc:
-                self._close_code = 1006
+                self._close_code = WSCloseCode.ABNORMAL_CLOSURE
                 self._exception = exc
                 return True
 
@@ -384,10 +385,10 @@ async def close(self, *, code: int = 1000, message: bytes = b"") -> bool:
                 async with async_timeout.timeout(self._timeout):
                     msg = await reader.read()
             except asyncio.CancelledError:
-                self._close_code = 1006
+                self._close_code = WSCloseCode.ABNORMAL_CLOSURE
                 raise
             except Exception as exc:
-                self._close_code = 1006
+                self._close_code = WSCloseCode.ABNORMAL_CLOSURE
                 self._exception = exc
                 return True
 
@@ -395,7 +396,7 @@ async def close(self, *, code: int = 1000, message: bytes = b"") -> bool:
                 self._close_code = msg.data
                 return True
 
-            self._close_code = 1006
+            self._close_code = WSCloseCode.ABNORMAL_CLOSURE
             self._exception = asyncio.TimeoutError()
             return True
         else:
@@ -430,10 +431,10 @@ async def receive(self, timeout: Optional[float] = None) -> WSMessage:
                     set_result(waiter, True)
                     self._waiting = None
             except (asyncio.CancelledError, asyncio.TimeoutError):
-                self._close_code = 1006
+                self._close_code = WSCloseCode.ABNORMAL_CLOSURE
                 raise
             except EofStream:
-                self._close_code = 1000
+                self._close_code = WSCloseCode.OK
                 await self.close()
                 return WSMessage(WSMsgType.CLOSED, None, None)
             except WebSocketError as exc:
@@ -443,7 +444,7 @@ async def receive(self, timeout: Optional[float] = None) -> WSMessage:
             except Exception as exc:
                 self._exception = exc
                 self._closing = True
-                self._close_code = 1006
+                self._close_code = WSCloseCode.ABNORMAL_CLOSURE
                 await self.close()
                 return WSMessage(WSMsgType.ERROR, exc, None)
 


Test patch:
diff --git a/tests/test_web_websocket_functional.py b/tests/test_web_websocket_functional.py
--- a/tests/test_web_websocket_functional.py
+++ b/tests/test_web_websocket_functional.py
@@ -6,7 +6,7 @@
 
 import aiohttp
 from aiohttp import WSServerHandshakeError, web
-from aiohttp.http import WSMsgType
+from aiohttp.http import WSCloseCode, WSMsgType
 
 
 async def test_websocket_can_prepare(loop, aiohttp_client) -> None:
@@ -153,11 +153,11 @@ async def handler(request):
 
     msg = await ws.receive()
     assert msg.type == aiohttp.WSMsgType.CLOSE
-    assert msg.data == 1000
+    assert msg.data == WSCloseCode.OK
     assert msg.extra == ""
 
     assert ws.closed
-    assert ws.close_code == 1000
+    assert ws.close_code == WSCloseCode.OK
 
     await closed
 
@@ -188,11 +188,11 @@ async def handler(request):
 
     msg = await ws.receive()
     assert msg.type == aiohttp.WSMsgType.CLOSE
-    assert msg.data == 1000
+    assert msg.data == WSCloseCode.OK
     assert msg.extra == ""
 
     assert ws.closed
-    assert ws.close_code == 1000
+    assert ws.close_code == WSCloseCode.OK
 
     await closed
 
@@ -223,7 +223,7 @@ async def handler(request):
 
     msg = await ws.receive()
     assert msg.type == aiohttp.WSMsgType.CLOSE
-    assert msg.data == 1000
+    assert msg.data == WSCloseCode.OK
     assert msg.extra == ""
 
     await ws.close()
@@ -244,7 +244,7 @@ async def handler(request):
         begin = ws._loop.time()
         assert await ws.close()
         elapsed = ws._loop.time() - begin
-        assert ws.close_code == 1006
+        assert ws.close_code == WSCloseCode.ABNORMAL_CLOSURE
         assert isinstance(ws.exception(), asyncio.TimeoutError)
         aborted.set_result(1)
         return ws
@@ -300,7 +300,7 @@ async def handler(request):
 
     ws = await client.ws_connect("/", autoclose=False, protocols=("eggs", "bar"))
 
-    await srv_ws.close(code=1007)
+    await srv_ws.close(code=WSCloseCode.INVALID_TEXT)
 
     msg = await ws.receive()
     assert msg.type == WSMsgType.CLOSE
@@ -321,7 +321,7 @@ async def handler(request):
 
         msg = await ws.receive()
         assert msg.type == WSMsgType.CLOSE
-        assert msg.data == 1000
+        assert msg.data == WSCloseCode.OK
         assert msg.extra == "exit message"
         closed.set_result(None)
         return ws
@@ -336,7 +336,7 @@ async def handler(request):
 
     msg = await ws.receive()
     assert msg.type == WSMsgType.PONG
-    await ws.close(code=1000, message="exit message")
+    await ws.close(code=WSCloseCode.OK, message="exit message")
     await closed
 
 
@@ -407,7 +407,7 @@ async def handler(request):
 
         msg = await ws.receive()
         assert msg.type == WSMsgType.CLOSE
-        assert msg.data == 1000
+        assert msg.data == WSCloseCode.OK
         assert msg.extra == "exit message"
         closed.set_result(None)
         return ws
@@ -423,7 +423,7 @@ async def handler(request):
     assert msg.type == WSMsgType.PONG
     assert msg.data == b"data"
 
-    await ws.close(code=1000, message="exit message")
+    await ws.close(code=WSCloseCode.OK, message="exit message")
 
     await closed
 
@@ -511,7 +511,7 @@ async def handler(request):
         assert not ws.closed
         await ws.close()
         assert ws.closed
-        assert ws.close_code == 1007
+        assert ws.close_code == WSCloseCode.INVALID_TEXT
 
         msg = await ws.receive()
         assert msg.type == WSMsgType.CLOSED
@@ -525,7 +525,7 @@ async def handler(request):
 
     ws = await client.ws_connect("/", autoclose=False, protocols=("eggs", "bar"))
 
-    await ws.close(code=1007)
+    await ws.close(code=WSCloseCode.INVALID_TEXT)
     msg = await ws.receive()
     assert msg.type == WSMsgType.CLOSED
     await closed