Skip to content

Instantly share code, notes, and snippets.

@rsalgado
Last active August 17, 2020 13:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rsalgado/76f5b94713b126005ac93a553207863a to your computer and use it in GitHub Desktop.
Save rsalgado/76f5b94713b126005ac93a553207863a to your computer and use it in GitHub Desktop.
SSE Handler Example with Plug and Cowboy 2

SSE Handler Example with Plug and Cowboy 2

To run in the console, without supervision trees, do the following:

$ iex -S mix

iex> Plug.Adapters.Cowboy2.http(NormalRouter, [], [dispatch: PlugDispatch.dispatch()])

To run inside a supervision tree, make sure to call child_spec (or use a tuple) with the correct arguments, like in the following example:

children = [
  {Plug.Adapters.Cowboy2, scheme: :http, plug: NormalRouter, options: [dispatch: PlugDispatch.dispatch()]}
]
  • Open http://localhost:4000/ to see NormalRouter in action.
  • Make a request to http://localhost:4000/sse to see SseHandler in action. Send OTP message to the connection process (The PID is printed at init).
  • Remember you can get PID from its numbers by using :c.pid/3. for example: :c.pid(0,123,0), assuming the PID is "0.123.0"
  • Example OTP message: send :c.pid(0,379,0), {:event, "This is an OTP event!!!\n" }
  • By default, idle connections are closed after 60s, and process gets killed

This handler was partially inspired by the gist: https://gist.github.com/alanpeabody/4fae12b420fb50376af4

defmodule SseHandler.MixProject do
use Mix.Project
def project do
[
app: :sse_handler,
version: "0.1.0",
elixir: "~> 1.6",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
cowboy: "~> 2.0",
plug: "~> 1.0"
]
end
end
defmodule NormalRouter do
use Plug.Router
plug :match
plug :dispatch
get "/" do
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, "Hello from NormalRouter!")
end
match _ do
conn |> send_resp(404, "Oops! Resource not found")
end
end
defmodule PlugDispatch do
@moduledoc """
This is a helper module to have the `dispatch` function in a convenient location
"""
@doc """
This returns the value for the dispatch option.
It manages the requests to `/sse` with `SseHandler`, while everything else is
managed with `NormalRouter`
"""
def dispatch do
[
{:_, [
{"/sse", SseHandler, []},
{:_, Plug.Adapters.Cowboy2.Handler, {NormalRouter, []}}
]}
]
end
end
defmodule SseHandler do
@moduledoc """
This is the handler responsible for SSE streaming. It implements the Cowboy
_loop handler_ behavior, by defining the callbacks `init` and `info`.
Take into account that `info` streaming could have been implemented
using any other pattern, and not necessarily `{:event, message}`.
The code is was translated (and modified/extended) from the snippets in the
**Loop Handlers chapter** of the **Cowboy User Guide**. Read more at:
https://ninenines.eu/docs/en/cowboy/2.4/guide/loop_handlers/
"""
def init(req, state) do
# Use this process' PID to send messages to its mailbox
IO.puts("SseHandler PID: #{inspect(self())}")
# This is part of the spec for SSE, but it would work without this header
req = :cowboy_req.set_resp_header("content-type", "text/event-stream", req)
# Start streaming
req = :cowboy_req.stream_reply(200, req)
# Indicate Cowboy that this is a Loop Handler
{:cowboy_loop, req, state, :hibernate}
end
# Stop streaming when :eof is received in the mailbox
def info(:eof, req, state) do
{:stop, req, state}
end
# Stream the event's message through SSE
def info({:event, message}, req, state) do
IO.puts("Event: #{message}")
body = "data: #{message}\n\n"
:cowboy_req.stream_body(body, :nofin, req)
{:ok, req, state}
end
# Ignore any other kind of message received in the mailbox by doing nothing
def info(msg, req, state) do
IO.puts("Message: #{msg}")
{:ok, req, state, :hibernate}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment