Disconnecting Django Channels

I’m trying to implement simple SSE with Django Channel’s AsyncHttpConsumer.

My minimal example:

class BaseSSEConsumer(AsyncHttpConsumer, ABC):

    cors_headers = [
        (b"Access-Control-Allow-Origin", b"*"),
        (b"Access-Control-Allow-Methods", b"GET, OPTIONS"),
        (
            b"Access-Control-Allow-Headers",
            b"Content-Type, Cache-Control, Authorization",
        ),
    ]

    default_headers = [
        (b"Cache-Control", b"no-cache"),
        (b"Content-Type", b"text/event-stream"),
        (b"Transfer-Encoding", b"chunked"),
        *cors_headers,
    ]

    async def handle(self, body):
        await self.send_headers(status=200, headers=self.default_headers)

        params = self._parse_query_params()

        message = {
            "type": "sse",
            "data": {
                "timestamp": datetime.now().isoformat(),
                "params": params,
            },
        }

        event = f"data: {json.dumps(message)}\n\n"

        while True:
            print(f"Sending SSE message")
            await self.send_body(body=event.encode(), more_body=True)
            await asyncio.sleep(1)

    async def disconnect(self, close_code=None):
        print("Disconnecting BaseSSEConsumer")

    def _parse_query_params(self) -> dict[str, list]:
        query_string = self.scope["query_string"].decode()
        return parse_qs(query_string)

I’m successfully sending events. When i close a browser’s tab the disconnect method doesn’t get called, instead after waiting for a few seconds i get following exception:

Application instance <Task pending name='Task-1' coro=<ASGIStaticFilesHandler.__call__() running at /usr/local/lib/python3.13/site-packages/django/contrib/staticfiles/handlers.py:101> wait_for=<Future pending cb=[Task.task_wakeup()]>> for connection <WebRequest at 0x7f5495d35d30 method=GET uri=/api/sse/?symbols=AAPL%2CGOOG%2CMSFT clientproto=HTTP/1.1> took too long to shut down and was killed.

I’m running the runserver with daphne.

Things i tried without success:

  • changing the server to uvicorn
  • offloading request processing to another thread

Is disconnecting from client supported by Channels? Should disconnect method be called after closing browser’s tab? Thanks!

Welcome @mikolajkapica !

My understanding is that SSE is fundamentally a different type of connection than a Websocket. SSE is a GET request with a special type of response as opposed to a Websocket which is effectively a TCP socket connection.

With an SSE response, if you don’t call .close from the client, you won’t get any notification that the connection is being dropped.

This is different than a Websocket. A Websocket being dropped will call the disconnect method.

I’ve made this simple html file with some js

<!DOCTYPE html>
<html>
  <head>
    <title>SSE Client</title>
  </head>
  <body>
    <h1>Server-Sent Events Example</h1>

    <div id="sse-data">
      <p>Waiting for SSE data...</p>
    </div>

    <script>
      const sseDataElement = document.getElementById("sse-data");

      if (!!window.EventSource) {
        const instruments = ["AAPL", "GOOG", "MSFT"];
        const params = new URLSearchParams();
        params.append("symbols", instruments.join(","));
        const eventSource = new EventSource(
          `http://localhost:8000/api/sse/?${params.toString()}`
        );

        eventSource.onmessage = function (event) {
          console.log("Received SSE message:", event.data);
          const newElement = document.createElement("p");
          newElement.textContent = `Data received: ${event.data}`;
          sseDataElement.appendChild(newElement);
        };

        eventSource.onerror = function (err) {
          console.error("EventSource failed:", err);
          eventSource.close(); // Close the connection on error
          const errorElement = document.createElement("p");
          errorElement.textContent =
            "Error with SSE connection. See console for details.";
          sseDataElement.appendChild(errorElement);
        };

        eventSource.onopen = function (event) {
          console.log("SSE connection opened.");
          const openMessage = document.createElement("p");
          openMessage.textContent = "SSE connection opened.";
          sseDataElement.appendChild(openMessage);
        };

        setTimeout(() => {
          console.log("Closing SSE connection after 5 seconds.");
          eventSource.close();
        }, 5000);

      } else {
        sseDataElement.innerHTML =
          "<p>Your browser does not support Server-Sent Events.</p>";
      }
    </script>
  </body>
</html>

Even after calling .close, the disconnect method isn’t called and i get the same error:

Application instance <Task pending name='Task-1' coro=<ASGIStaticFilesHandler.__call__() running at /usr/local/lib/python3.13/site-packages/django/contrib/staticfiles/handlers.py:101> wait_for=<Future pending cb=[Task.task_wakeup()]>> for connection <WebRequest at 0x7f22df14dbe0 method=GET uri=/api/sse/?symbols=AAPL%2CGOOG%2CMSFT clientproto=HTTP/1.1> took too long to shut down and was killed.

Maybe i could provide some more details?

Ok, there may be something with the handling of SSEs where even calling .close doesn’t allow it to work - I don’t really know, because the limitations associated with SSE make them a technology that I’ve never bothered work with or learn about.

Hopefully someone else here can shed more light on this.

I feel like some signal is being captured, which is causing the error. I can’t figure out how to avoid or handle it, though.

Thanks for the response anyway! It would be wonderful to have someone take a look.