The Fix
Upgrade to version 0.20.0 or later.
Based on closed Kludex/uvicorn issue #596 · PR/commit linked
@@ -82,7 +82,7 @@ plugins =
[coverage:report]
precision = 2
-fail_under = 97.82
+fail_under = 97.92
show_missing = true
import asyncio
async def app(scope, receive, send):
assert scope["type"] == "websocket"
event = await receive()
if event["type"] == "websocket.connect":
await asyncio.sleep(10)
await send({"type": "websocket.accept"})
Re-run: uvicorn debug.app:app --ws wsproto
Option A — Upgrade to fixed release\nUpgrade to version 0.20.0 or later.\nWhen NOT to use: Do not use this fix if the application requires immediate disconnection without waiting for handshake completion.\n\n
Why This Fix Works in Production
- Trigger: LocalProtocolError: Event CloseConnection(code=1012, reason=None) cannot be sent during the handshake
- Mechanism: The handshake was not completed before attempting to send a frame during shutdown
- Why the fix works: Checks if the handshake is completed before sending a frame on wsproto shutdown, addressing the issue of LocalProtocolError during shutdown. (first fixed release: 0.20.0).
Why This Breaks in Prod
- Shows up under Python 3.9 in real deployments (not just unit tests).
- The handshake was not completed before attempting to send a frame during shutdown
- Surfaces as: LocalProtocolError: Event CloseConnection(code=1012, reason=None) cannot be sent during the handshake
Proof / Evidence
- GitHub issue: #596
- Fix PR: https://github.com/kludex/uvicorn/pull/1737
- First fixed release: 0.20.0
- Reproduced locally: No (not executed)
- Last verified: 2026-02-09
- Confidence: 0.95
- Did this fix it?: Yes (upstream fix exists)
- Own content ratio: 0.30
Discussion
High-signal excerpts from the issue thread (symptoms, repros, edge-cases).
“@euri10 Yes, it happens every time we send TERM to gunicorn master process in our production environment”
“The minimal app: 1”
“on a side note thanks for websocat, it rocks”
“sorry I cant reproduce, am I doing then in another terminal and before the 10s in a 3rd one:”
Failure Signature (Search String)
- LocalProtocolError: Event CloseConnection(code=1012, reason=None) cannot be sent during the handshake
Error Message
Stack trace
Error Message
-------------
LocalProtocolError: Event CloseConnection(code=1012, reason=None) cannot be sent during the handshake
File "gunicorn/arbiter.py", line 583, in spawn_worker
worker.init_process()
File "uvicorn/workers.py", line 57, in init_process
super(UvicornWorker, self).init_process()
File "gunicorn/workers/base.py", line 140, in init_process
self.run()
File "uvicorn/workers.py", line 66, in run
loop.run_until_complete(server.serve(sockets=self.sockets))
File "uvloop/loop.pyx", line 1456, in uvloop.loop.Loop.run_until_complete
File "uvicorn/main.py", line 403, in serve
await self.shutdown(sockets=sockets)
File "uvicorn/main.py", line 539, in shutdown
connection.shutdown()
File "uvicorn/protocols/websockets/wsproto_impl.py", line 115, in shutdown
output = self.conn.send(wsproto.events.CloseConnection(code=1012))
File "__init__.py", line 61, in send
data += self.handshake.send(event)
File "wsproto/handshake.py", line 101, in send
"Event {} cannot be sent during the handshake".format(event)
Stack trace
Error Message
-------------
$ uvicorn debug.app:app --ws wsproto
INFO: Started server process [91950]
INFO: Waiting for application startup.
INFO: ASGI 'lifespan' protocol appears unsupported.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Go ahead, stop the server...
^CINFO: Shutting down
Traceback (most recent call last):
File "/Users/florimond/Developer/python-projects/uvicorn/venv/bin/uvicorn", line 33, in <module>
sys.exit(load_entry_point('uvicorn', 'console_scripts', 'uvicorn')())
File "/Users/florimond/Developer/python-projects/uvicorn/venv/lib/python3.9/site-packages/click/core.py", line 829, in __call__
return self.main(*args, **kwargs)
File "/Users/florimond/Developer/python-projects/uvicorn/venv/lib/python3.9/site-packages/click/core.py", line 782, in main
rv = self.invoke(ctx)
File "/Users/florimond/Developer/python-projects/uvicorn/venv/lib/python3.9/site-packages/click/core.py", line 1066, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "/Users/florimond/Developer/python-projects/uvicorn/venv/lib/python3.9/site-packages/click/core.py", line 610, in invoke
return callback(*args, **kwargs)
File "/Users/florimond/Developer/python-projects/uvicorn/uvicorn/main.py", line 362, in main
run(**kwargs)
File "/Users/florimond/Developer/python-projects/uvicorn/uvicorn/m
... (truncated) ...
Minimal Reproduction
import asyncio
async def app(scope, receive, send):
assert scope["type"] == "websocket"
event = await receive()
if event["type"] == "websocket.connect":
await asyncio.sleep(10)
await send({"type": "websocket.accept"})
Environment
- Python: 3.9
What Broke
LocalProtocolError occurs during shutdown, causing websocket connections to fail.
Why It Broke
The handshake was not completed before attempting to send a frame during shutdown
Fix Options (Details)
Option A — Upgrade to fixed release Safe default (recommended)
Upgrade to version 0.20.0 or later.
Use when you can deploy the upstream fix. It is usually lower-risk than long-lived workarounds.
Fix reference: https://github.com/kludex/uvicorn/pull/1737
First fixed release: 0.20.0
Last verified: 2026-02-09. Validate in your environment.
When NOT to Use This Fix
- Do not use this fix if the application requires immediate disconnection without waiting for handshake completion.
Verify Fix
Re-run: uvicorn debug.app:app --ws wsproto
Did This Fix Work in Your Case?
Quick signal helps us prioritize which fixes to verify and improve.
Prevention
- Add a TLS smoke test that performs a real handshake in CI (include CA bundle validation and hostname checks).
- Alert on handshake failures by error string and endpoint to catch cert/CA changes quickly.
Version Compatibility Table
| Version | Status |
|---|---|
| 0.20.0 | Fixed |
Related Issues
No related fixes found.
Sources
We don’t republish the full GitHub discussion text. Use the links above for context.