My ASGI app sends events fine to curl, and to my phone. However, even though the server is sending the events, and the headers look right, neither Firefox nor Chrome on my Windows machine receives the events until the connection is closed.
This happens whether I host the server in WSL, in Powershell terminal, or on a separate Linux box.
However, those same browsers work fine if I host the server on repl.it (please fork it and try it out).
I have tried fiddling with Windows firewall settings, to no avail.
Here is the application code:
import asyncio
import datetime
async def app(scope, receive, send):
headers = [(b"content-type", b"text/html")]
if scope["path"] == "/":
body = (
"<html>"
"<body>"
"</body>"
"<script>"
" let eventSource = new EventSource('/sse');"
" eventSource.addEventListener('message', (e) => {"
" document.body.innerHTML += e.data + '<br>';"
" });"
"</script>"
"</html>"
).encode()
await send({"type": "http.response.start", "status": 200, "headers": headers})
await send({"type": "http.response.body", "body": body})
elif scope["path"] == "/sse":
headers = [
(b"content-type", b"text/event-stream"),
(b"cache-control", b"no-cache"),
(b"connection", b"keep-alive"),
]
async def body():
ongoing = True
while ongoing:
try:
payload = datetime.datetime.now()
yield f"data: {payload}\n\n".encode()
await asyncio.sleep(10)
except asyncio.CancelledError:
ongoing = False
await send({"type": "http.response.start", "status": 200, "headers": headers})
async for chunk in body():
await send({"type": "http.response.body", "body": chunk, "more_body": True})
await send({"type": "http.response.body", "body": b""})
else:
await send({"type": "http.response.start", "status": 404, "headers": headers})
await send({"type": "http.response.body", "body": b""})
This can be run by naming the file above to asgi_sse.py
, then pip install uvicorn
, then using something like
uvicorn asgi_sse:app
(substitute daphne
or hypercorn
instead of uvicorn
above to see how those servers handle the app.)
The headers:
$ curl -I http://localhost:8000/sse
HTTP/1.1 200 OK
date: Mon, 01 Jun 2020 09:51:41 GMT
server: uvicorn
content-type: text/event-stream
cache-control: no-cache
connection: keep-alive
And the response:
$ curl http://localhost:8000/sse
data: 2020-06-01 05:52:40.735403
data: 2020-06-01 05:52:50.736378
data: 2020-06-01 05:53:00.736812
Any insights are quite welcome!
My company has Sophos Endpoint Security's web protection turned on. According to this entry in Sophos's community, the web protection buffers and scans text/event-stream content for malware. Hence the unexpected buffering.
There are two workarounds I have found:
The above issue was not simply an issue with my code, nor with Uvicorn, Hypercorn, or ASGI. In fact, I even tried an aiohttp implementation, with the same sad results. However, when I tried a Go example implementation of SSE, and another in Node.js it worked fine, no workarounds needed. The only difference I can see was that the Go implementation used a flush method after each event. I am uncertain why ASGI and aiohttp do not expose some sort of flush method, or, if they do, why I cannot find it. If they did, would it make these workarounds unnecessary? I am unsure.
Here is the updated code that makes it work with Sophos, and checks if serving over https or not:
async def app(scope, receive, send):
headers = [(b"content-type", b"text/html")]
if scope["path"] == "/":
body = (
"<!DOCTYPE html>"
"<html>"
"<body>"
"</body>"
"<script>"
" let eventSource = new EventSource('/sse');"
" eventSource.addEventListener('message', (e) => {"
" document.body.innerHTML += e.data + '<br>';"
" });"
"</script>"
"</html>"
).encode()
await send({"type": "http.response.start", "status": 200, "headers": headers})
await send({"type": "http.response.body", "body": body})
elif scope["path"] == "/sse":
headers = [
(b"Content-Type", b"text/event-stream"),
(b"Cache-Control", b"no-cache"),
(b"Connection", b"keep-alive"),
]
async def body():
ongoing = True
while ongoing:
try:
payload = datetime.datetime.now()
yield f"data: {payload}\n\n".encode()
await asyncio.sleep(10)
except asyncio.CancelledError:
ongoing = False
await send({"type": "http.response.start", "status": 200, "headers": headers})
if scope["scheme"] != "https": # Sophos will buffer, so send 2MB first
two_meg_chunk = "." * 2048 ** 2
await send(
{
"type": "http.response.body",
"body": f": {two_meg_chunk}\n\n".encode(),
"more_body": True,
}
)
async for chunk in body():
await send({"type": "http.response.body", "body": chunk, "more_body": True})
await send({"type": "http.response.body", "body": b""})
else:
await send({"type": "http.response.start", "status": 404, "headers": headers})
await send({"type": "http.response.body", "body": b""})