Django 5.1, ASGI, SSE
Hello.
Sorry, I have a question about Django and ASGI. I saw that a similar topic has already been discussed, but I couldn't find a solution for myself.
Unit 1.33 Django 5.1 (support async handling disconnects) Python 3.11.2 Debian 12
Test Django code:
def sse(request: HttpRequest) -> HttpResponseBase:
"""Small demo of the basic idea of SSE without any redis or other complexity"""
async def stream(request: HttpRequest) -> AsyncGenerator[str, None]:
try:
counter = 0
while True:
counter += 1
if counter != 1:
await asyncio.sleep(1.0)
yield f"data: <div>{counter}</div>\n\n"
except asyncio.CancelledError:
logger.debug("SSE Client disconnected")
raise
return StreamingHttpResponse(
streaming_content=stream(request),
content_type="text/event-stream"
)
When I run this code under the Uvicorn ASGI server, after closing the connection, I see "SSE Client disconnected" in my dubug log. However, under Unit, nothing happens, with only in unit log:
2024/12/01 11:29:31 [info] 40085#40121 *11 writev(44, 2) failed (32: Broken pipe)
It seems that Django does not detect that the connection is closed, and this is only apparent when I restart Unit. In the logs:
2024/12/01 11:02:55 [warn] 34253#34253 [unit] #516: active request on ctx quit
2024/12/01 11:02:55 [warn] 34253#34253 [unit] #404: active request on ctx free
In a production environment when I use Redis, I see many connections with Unit and Redis in the ESTABLISHED state, even though many connections are already closed. This is a problem for me.
What am I doing wrong 🤔 ?
small addon.
this is my debug ASGI entry point:
import os
from loguru import logger
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'conf.settings')
django_application = get_asgi_application()
async def application(scope, receive, send):
if scope['type'] == 'http':
# Let Django handle only ASGI HTTP requests
async def receive_wrapper():
message = await receive()
logger.debug(f'ASGI application receive message: {message}')
await logger.complete()
return message
await django_application(scope, receive_wrapper, send)
else:
pass
When I connected, I see:
ASGI application receive message: {'type': 'http.request'}
But when I disconnected - log is empty.
As I said, if I run this test under Uvicorn, I see:
ASGI application receive message: {'type': 'http.disconnect'}
Where I lost 'http.disconnect' while working with Unit ?
@ac000 I have opened a draft pr for this issue. Not sure, it is right approach or not but have tried to come up with a possible solution. Could you please review it once. Have added more details as description and commit msg https://github.com/nginx/unit/pull/1556/files
@mamashin
~~Do you have a simple purely ASGI reproducer (no frameworks involved) and steps to reproduce?~~
Nevermind, I have one from @gourav-kandoria
I.e. something that simply shows the Python app is not getting notified when clients close the socket?
Hmm, I'm seeing something slightly different...
With
async def application(scope, receive, send):
while True:
m = await receive()
if m['type'] == 'http.disconnect':
print("Client Disconnect")
break
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [[b"content-type", b"text/plain"]],
}
)
await send({"type": "http.response.body", "body": b"Hello, world!"})
I get the "Client Disconnect" message after every request, even though Firefox is using keep-alive.
Indeed, unlike with WebSockets where Firefox will close the connection if you close the browser tab, Firefox seems to keep the socket open even if you close the tab, if you restore the tab, it continues using the same socket.
Not sure why we're sending a http.disconnect after request though...
Seems to be something peculiar to the way Server-Sent Events are handled...