HTMX server sent events with rails
  • This took me longer than it should have to figure out, so leaving a note here for future reference.

  • Context

  • Sometimes we may want to stream content into a web page as it arrives. The best example of this is generations from AI APIs, where you want the text to appear as it is received from the API. Web browsers have a feature called Server Sent Events that allow us to do this. Rails lets us send these responses from our controller, and htmx lets us stream the responses into our page.

  • Steps

  • Add the htmx SSE plugin. Make sure that it's loading after the main htmx file, and make sure the version reflects your version of htmx - they changed the syntax after 1.9.9

  • <script src="/vendor/htmx/2.0.4.js" defer ></script>
    <script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js" defer></script>
  • In your view, create a div that fetches a streaming response from the server.

  • <div hx-disinherit="*">
    
      <div  hx-ext="sse" 
            sse-connect="<%= demo_stream_chat_path %>" 
            sse-swap="message"
            sse-swap-style="append"
            hx-swap="beforeend"
            sse-close="complete">
          The response is: 
      </div>
    
    </div>
  • What's happening here: We're saying "open a connection to /chat_demo, then for every message you receive, put it into this div, and once you receive a message called complete, close the connection.

  • Now that our view is set up, we'll create a controller action to send content back.

  • Syntax A

  • This is more close to the bare metal, but it can be brittle. It will break if you either leave out the Last-Modified header, misformat the string, or leave out the trailing \n\ns in the string

  • class StreamingController < ApplicationController
    
      include ActionController::Live
    
      def stream_chat
        response.headers['Content-Type'] = 'text/event-stream'
        response.headers['Last-Modified'] = Time.now.httpdate
      
        response.stream.write("data: Message 1\n\n")
        sleep 2
        response.stream.write("data: Message 2\n\n")
        sleep 2 
        response.stream.write("data: Message 3\n\n")
        response.stream.write("event: complete\ndata: close\n\n")
      ensure
        response.stream.close
      end
    
    end
  • Syntax B

  • class StreamingController < ApplicationController
    
      include ActionController::Live
    
      def stream_chat
        response.headers['Content-Type'] = 'text/event-stream'
        response.headers['Last-Modified'] = Time.now.httpdate
    
        sse = SSE.new(response.stream)
    
        sse.write('Message 1')
        sleep 2
        sse.write('Message 2')
        sleep 2
        sse.write('Message 3')
        
        sse.write('done', event: 'complete')
      ensure
        sse.close
      end
    
    end

  • Website Page