fanout / django-eventstream

Server-Sent Events for Django
MIT License
638 stars 84 forks source link

Compatibility with htmx #134

Open aranvir opened 7 months ago

aranvir commented 7 months ago

Hi, I've been trying to use django-eventstream with the htmx sse extension, but can't get it to work quite. I did look into the examples and managed to get those running so the principle environment setup should be fine. I am a beginner for both packages and also django in general so I might miss the obvious.

When I start the app and open the page I can see it connecting to the /events endpoint successfully. However, in the browser console I keep getting error message. It periodically sends another get-request to /events and while the returncode is 200 it seems like htmx does not like the response. If I manually go to /events I get::

event: stream-error
id: error
data: {"condition": "bad-request", "text": "Invalid request: No channels specified."}

No I don't know if htmx faces the same issue or if I only get this because my manual request does not specify a channel. Yet I don't even know how I would do that. I understood from the documentation that it is encouraged to use channels and it is somewhat clear how they are defined and how I can send messages to a channel but I don't understand how the client part selects the channel.

Maybe it's a htmx limitation? But even if I don't use channels, I don't get a proper reply. Would appreciate any help or tips. Or maybe I need to bring this question to the htmx github.

I appended the relevant code below:

mydemo/asgi.py

"""
ASGI config for mydemo project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""

import os
import django
from django.core.asgi import get_asgi_application
from django.urls import path, re_path
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import django_eventstream

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings")

application = ProtocolTypeRouter({
    'http': URLRouter([
        path('events/', AuthMiddlewareStack(
            URLRouter(django_eventstream.routing.urlpatterns)
        ), {'channels': ['test']}),
        re_path(r'', get_asgi_application()),
    ]),
})

mydemo/settings.py

...
INSTALLED_APPS = [
    'daphne',
    'channels',
    'django_eventstream',
    'server_events',
    ...
]
...
MIDDLEWARE = [
    'django_grip.GripMiddleware',
    ...
]
...
ASGI_APPLICATION = "mydemo.asgi.application"

mydemo/urls.py

from django.contrib import admin
from django.urls import path, include
from server_events import views

urlpatterns = [
    path('', include('server_events.urls')),
    path('admin/', admin.site.urls),
]

server_events/urls.py

from django.urls import path, include
import django_eventstream

from . import views

app_name = "server_events"
urlpatterns = [
    path("", views.index, name="index"),
    path("update", views.update),
    path(
        "events",
        include(django_eventstream.urls),
        {"channels": ["test"]}
    )
]

server_events/views.py

from django.shortcuts import render
from django.http import HttpResponse
from django_eventstream import send_event

# Create your views here.
def index(request):
    return render(request, 'server_events/index.html')

COUNTER = 0

def update(request):
    global COUNTER
    COUNTER += 1
    print("SENDING")
    send_event("test", "message", f"<p>Test: {COUNTER}<p>")
    return HttpResponse()

server_events/templates/server_events/index.html

<!DOCTYPE html>
{% load static %}
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://unpkg.com/htmx.org@1.9.8" integrity="sha384-rgjA7mptc2ETQqXoYC3/zJvkU7K/aP44Y+z7xQuJiVnB/422P/Ak+F/AqFR7E4Wr" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script>
    <link rel="stylesheet" href="https://cdn.simplecss.org/simple.css">
</head>
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<h1>Hello World</h1>
  <button hx-get="update" hx-swap="none">Update</button>
  <div hx-ext="sse" sse-connect="/events" sse-swap="test">
      Contents of this box will be updated in real time
      with every SSE message received from the chatroom.
  </div>
</body>
</html>
xtlc commented 7 months ago

have you been able to resolve this?

aranvir commented 7 months ago

Nope, I also stopped trying.

However, I think I did test a similar setup with FastAPI and that worked out of the box. So, I assume that the issue is that the message format of htmx is different from what django-eventstream expects and I don't have the expertise to look deeper into that.

jkarneges commented 7 months ago

At a glance I don't see anything wrong with the code. The "time" example specifies the channel the same way. It's not a format compatibility issue since the error happens before any messages are sent.

If you want to debug further you could see if this method is getting called or not and what it is returning.

msmart commented 4 months ago

a boilderplate htmx setup worked for me with channels 3.0.5:

send_event(
        "test",
        "message",
        "<div>Content to swap into your HTML page.</div>",
        json_encode=False,
    )

I added json_code=False to ensure that the payload is just a string. Maybe you where missing the trailing slash in /events/ ?

rexzhang commented 2 months ago

work with uvicorn+htmx so easy, thanks this project!

urls.py

path("sse/status", include(django_eventstream.urls), {"channels": ["status"]}),

action.py

send_event("status", "message", "Check/Update is running", json_encode=False)
...
send_event("status", "message", "", json_encode=False)

_status.html

<span hx-ext="sse"
      hx-swap="innerHTML"
      sse-connect="/sse/status"
      sse-swap="message"
      class="badge rounded-pill text-bg-warning mx-1"></span>