Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle case when websocket client disconnects before websocket connection is accepted #2312

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ description = "The lightning-fast ASGI server."
readme = "README.md"
license = "BSD-3-Clause"
requires-python = ">=3.8"
authors = [
{ name = "Tom Christie", email = "[email protected]" },
]
authors = [{ name = "Tom Christie", email = "[email protected]" }]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
Expand Down Expand Up @@ -90,16 +88,14 @@ filterwarnings = [
"error",
'ignore: \"watchgod\" is deprecated\, you should switch to watchfiles \(`pip install watchfiles`\)\.:DeprecationWarning',
"ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning",
"ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning"
"ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning",
]

[tool.coverage.run]
branch = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this, and formatting the file. Coverage may drop.

source_pkgs = ["uvicorn", "tests"]
plugins = ["coverage_conditional_plugin"]
omit = [
"uvicorn/workers.py",
"uvicorn/__main__.py",
]
omit = ["uvicorn/workers.py", "uvicorn/__main__.py"]

[tool.coverage.report]
precision = 2
Expand All @@ -124,7 +120,7 @@ py-not-win32 = "sys_platform != 'win32'"
py-linux = "sys_platform == 'linux'"
py-darwin = "sys_platform == 'darwin'"
py-gte-38 = "sys_version_info >= (3, 8)"
py-lt-38 = "sys_version_info < (3, 8)"
py-lt-38 = "sys_version_info < (3, 8)"
py-gte-39 = "sys_version_info >= (3, 9)"
py-lt-39 = "sys_version_info < (3, 9)"
py-gte-310 = "sys_version_info >= (3, 10)"
Expand Down
39 changes: 32 additions & 7 deletions tests/protocols/test_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ async def asgi(self):
break


async def wsresponse(url):
async def wsresponse(url: str):
"""
A simple websocket connection request and response helper
"""
Expand Down Expand Up @@ -507,6 +507,34 @@ async def connect(url: str):
assert exc_info.value.status_code == 500


@pytest.mark.anyio
async def test_do_not_send_500_on_closed_transport(
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
):
is_disconnected = asyncio.Event()
is_inside_app = asyncio.Event()

async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
# The client can only disconnect after we've reached the application.
is_inside_app.set()
await asyncio.sleep(0.1)
# Wait for client to disconnect.
await is_disconnected.wait()

async def connect(url: str):
await websockets.client.connect(url)

config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
task = asyncio.create_task(connect(f"ws://127.0.0.1:{unused_tcp_port}"))
await is_inside_app.wait()
task.cancel()
is_disconnected.set()
# Sleep here so the server has a chance to react before shutting down.
await asyncio.sleep(0.1)
assert task.cancelled()


@pytest.mark.anyio
async def test_send_before_handshake(
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
Expand Down Expand Up @@ -1069,11 +1097,6 @@ async def app(scope, receive, send):
}
await send(message)

async def websocket_session(url):
response = await wsresponse(url)
assert response.status_code == 500
assert response.content == b"Internal Server Error"

config = Config(
app=app,
ws=ws_protocol_cls,
Expand All @@ -1082,7 +1105,9 @@ async def websocket_session(url):
port=unused_tcp_port,
)
async with run_server(config):
await websocket_session(f"ws://127.0.0.1:{unused_tcp_port}")
response = await wsresponse(f"ws://127.0.0.1:{unused_tcp_port}")
assert response.status_code == 500
assert response.content == b"Internal Server Error"


@pytest.mark.anyio
Expand Down
3 changes: 2 additions & 1 deletion uvicorn/protocols/websockets/websockets_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ def send_500_response(self) -> None:
b"\r\n",
msg,
]
self.transport.write(b"".join(content))
if not self.transport.is_closing():
self.transport.write(b"".join(content))
# Allow handler task to terminate cleanly, as websockets doesn't cancel it by
# itself (see https://github.com/encode/uvicorn/issues/920)
self.handshake_started_event.set()
Expand Down
1 change: 1 addition & 0 deletions uvicorn/protocols/websockets/wsproto_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ def send_500_response(self) -> None:
]
output = self.conn.send(wsproto.events.RejectConnection(status_code=500, headers=headers, has_body=True))
output += self.conn.send(wsproto.events.RejectData(data=b"Internal Server Error"))
print('is here')
self.transport.write(output)

async def run_asgi(self) -> None:
Expand Down
Loading