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.
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