Search code examples
apiserver-sent-eventsruby-on-rails-6puma

Server-sent events in Rails not delivered asynchronously


I'm trying to write an API that delivers server-sent events using ActionController::Live::SSE in Rails 6. In order to understand how the tests would best be written, I started with essentially copying the trivial example seen here: my_controller.rb:

class MyController < ApplicationController
  include ActionController::Live
  def capture
    response.headers['Content-Type'] = 'text/event-stream'
    sse = SSE.new(response.stream)
    3.times do
      sse.write({message: "Awaiting confirmation ..."})
      sleep 2
    end
    fake_response  = { #The response as hash.
     "annotation_id"=>nil,
     "domain"=>"some.random.com",
     "id"=>2216354,
     "path"=>"/flummoxer/",
     "protocol"=>"https",
    }
    sse.write(fake_response, event: 'successful capture')
  rescue => e
    sse.write(e.message, event: 'something broke: ')
  ensure
    response.stream.close
  end
end

When I send a curl request (whether I make it POST or GET) to this endpoint the response arrives all in one chunk, rather than as separate responses:

$ curl -i -X GET -H  "Content-Type: application/json" -d '{"url": "https://some.random.com/flummoxer"}' http://localhost:3000/capture
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
ETag: W/"a24048695d2feca40232467f0fbb410a"
X-Request-Id: 648a5229-a43d-40d3-82fd-1c4ea6fe19cc
X-Runtime: 24.082528
Transfer-Encoding: chunked

data: {"message":"Awaiting confirmation ..."}

data: {"message":"Awaiting confirmation ..."}

data: {"message":"Awaiting confirmation ..."}

event: successful capture
data: {"annotation_id":null,"domain":"some.random.com","id":2216354,"path":"/flummoxer/","protocol":"https"}

This can more easily be seen by the fact that in my test, attempting to parse the response from my server fails:

MultiJson::ParseError: 783: unexpected token at 'data: {"message":"Awaiting confirmation ..."}

data: {"message":"Awaiting confirmation ..."}

data: {"message":"Awaiting confirmation ..."}

event: successful capture
data: {"annotation_id":null,"domain":"some.random.com","id":2216354,"path":"/flummoxer/","protocol":"https"}
'

My server is Puma, so it's not because I'm using Thin, as seen in this answer.

What am I doing wrong? I'll provide any additional information that might be of use, if you ask.

UPDATE: The answers to this question suggest adding both the -N and the Accept:text/event-stream header to the request. Doing so doesn't change the behavior I've described above -- the response to the request isn't sent until the call to response.stream.close is fired.

UPDATE 2: I've also tried hacking the SSE#write method to call broadcast() on the Mutex::ConditionVariable to force sending the message. This works, in the sense that it sends data immediately, but has the side effect of the curl request thinking that the stream is closed, and so no further messages are sent, which is not a stream.

UPDATE 3: I've also modified development.rb to include config.allow_concurrency = true, as seen here. There's no change in the behavior described above.


Solution

  • I ran into a similar issue with a basic 'out the book' Rails 5 SSE app. The issue turned out to be a Rack update that lead to buffering of the stream. More info here https://github.com/rack/rack/issues/1619 and fixed by including

    config.middleware.delete Rack::ETag

    in config/application.rb