Skip to content

Instantly share code, notes, and snippets.

@rbishop
Last active April 26, 2022 15:38
Show Gist options
  • Save rbishop/e7b1886d5e75b2f74d8b to your computer and use it in GitHub Desktop.
Save rbishop/e7b1886d5e75b2f74d8b to your computer and use it in GitHub Desktop.
A super simple Elixir server for sending Server Sent Events to the browser.

Generate a new Elixir project using mix and add cowboy and plug as dependencies in mix.exs:

  defp deps do
    [
      {:cowboy, "~> 1.0.0"},
      {:plug, "~> 0.8.1"}
    ]
  end

Then add the sse.ex file below to lib/ and run the code with mix run --no-halt lib/sse.ex. Point your browser to http://localhost:4000 and you're done.

If you don't feel like writing your own EventSource JavaScript, you can use the example in index.html below that comes from Cowboy. Just create a priv/static directory inside of your project.

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
function ready() {
if (!!window.EventSource) {
setupEventSource();
} else {
document.getElementById('status').innerHTML =
"Sorry but your browser doesn't support the EventSource API";
}
}
function setupEventSource() {
var source = new EventSource('/sse');
source.addEventListener('message', function(event) {
addStatus("server sent the following: '" + event.data + "'");
}, false);
source.addEventListener('open', function(event) {
addStatus('eventsource connected.')
}, false);
source.addEventListener('error', function(event) {
if (event.eventPhase == EventSource.CLOSED) {
addStatus('eventsource was closed.')
}
}, false);
}
function addStatus(text) {
var date = new Date();
document.getElementById('status').innerHTML
= document.getElementById('status').innerHTML
+ date + ": " + text + "<br/>";
}
</script>
</head>
<body onload="ready();">
Hi!
<div id="status"></div>
</body>
</html>
defmodule Sse do
import Plug.Conn
use Plug.Router
plug :match
plug :dispatch
get "/" do
conn
|> put_resp_header("content-type", "text/html")
|> send_file(200, "priv/static/index.html")
end
get "/sse" do
conn = put_resp_header(conn, "content-type", "text/event-stream")
conn = send_chunked(conn, 200)
send_message(conn, "Look, Ma'! I'm streaming!")
:timer.sleep(1000)
send_message(conn, "It only took two lines of code!")
:timer.sleep(1000)
send_message(conn, "All you have to do is set a header and chunk the response!")
:timer.sleep(1000)
send_message(conn, "Bye now!")
conn
end
defp send_message(conn, message) do
chunk(conn, "event: \"message\"\n\ndata: {\"message\": \"#{message}\"}\n\n")
end
end
# Run with mix run --no-halt lib/sse.ex
Plug.Adapters.Cowboy.http Sse, [], port: 4000
@szymon-jez
Copy link

Looks very good. An example how to connect to the server from an Elixir client would make this even better and complete.

@CrowdHailer
Copy link

CrowdHailer commented Jun 25, 2016

Just wondering. In the latest version of Plug should sent_message be replaced by chunk?

Copy link

ghost commented Jan 20, 2020

Great!!! it is working but, after 1 minute minute the http sse session is closed even if i have code like this: WHAT CAN I DO?, WHAT DOCUMENTACION CAN I REVIEW?

conn = put_resp_header(conn, "content-type", "text/event-stream")
conn = put_resp_header(conn, "cache-control", "no-cache")
conn = put_resp_header(conn, "connection", "keep-alive")
conn = send_chunked(conn, 200)
send_message(conn, "Look, Ma'! I am streaming")
:timer.sleep(10000)
send_message(conn, "It only took two lines of code!")
:timer.sleep(10000)
send_message(conn, "All you have to do is set a header and chunk the response!1")
:timer.sleep(10000)
send_message(conn, "All you have to do is set a header and chunk the response!2")
:timer.sleep(10000)
send_message(conn, "All you have to do is set a header and chunk the response!3")
:timer.sleep(10000)
send_message(conn, "All you have to do is set a header and chunk the response!4")
:timer.sleep(10000)
send_message(conn, "All you have to do is set a header and chunk the response!5")
:timer.sleep(10000)
send_message(conn, "All you have to do is set a header and chunk the response!6")
:timer.sleep(10000)
send_message(conn, "All you have to do is set a header and chunk the response!7")
:timer.sleep(10000)
send_message(conn, "All you have to do is set a header and chunk the response!8")

WHAT CAN I DO?, WHAT DOCUMENTACION CAN I REVIEW?

@rbishop
Copy link
Author

rbishop commented Jan 21, 2020

This is over 5 years old so I'm surprised it works at all. When I made this long ago, I read through Plug's source code. I'd recommend starting there. I wish I could be more help but I don't have time and haven't touched Elixir in 2+ years.

@KristerV
Copy link

KristerV commented Apr 15, 2020

lol, I'm looking into SSE now and google led me here :)
what are you using these days and why?

edit: This still works in 2020. I have the same problem as victor, but I'll figure it out.
edit2: now I understand victor's frustration. No headers, sending kep-alive events - connection still dies..
edit3: Okay it's because of Cowboy. Fix is to add protocol_options: [idle_timeout: :infinity] to cowboy options. Not ideal though. Can't find a way to set route-specific option.

I've documented the process in my blog since this shouldn't take more than an hour and it took me 3 days.

@KristerV
Copy link

KristerV commented May 5, 2020

Soo I spent a good week implementing this into my system, and THEN it turned out, that browsers limit the number of SSE connections to 6!!! wtf why 6? Why a limit at all? Well, here's more details: https://stackoverflow.com/questions/5195452/websockets-vs-server-sent-events-eventsource

I'm switching to WebSockets.

@rbishop
Copy link
Author

rbishop commented May 5, 2020

That’s a bummer to hear and thank you for the update in your other comment above. It’s too bad browsers never gave SSE a fair chance. It strikes a really nice balance between what users actually want and complexity.

@HamptonMakes
Copy link

@KristerV And as I'm writing from teh future, I'm sure you also learned of the overhead with websockets!

@KristerV
Copy link

KristerV commented Nov 7, 2020

what overhead do you mean exactly?

@HamptonMakes
Copy link

Oh, holding open WS connections is very expensive. Also, the SSE limit of 6 only applies to HTTP/1 connections and assuming you aren't using a CDN to route your SSE.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment