Mixing WebSockets & HTTP endpoints in Elixir with Cowboy + Plug
July 20, 2023
5 min read
750 views
Howdy! š¤
I recently found myself wanting to add a WebSocket endpoint to an existing Plug-based Elixir API I'd built with Cowboy. It seemed like a straightforward task, but I found pretty sparse information online about it. After a weekend spent looking through Hexdocs & Cowboy's documentation, I figured I'd write a short post about my solution.
My reason for using Cowboy in the first place comes partly from stubbornness. Frameworks like Phoenix offer useful abstractions and utilities, but I prefer the lightweight & more minimalist approach provided by libraries like Cowboy. I find it gives more granular control over request handling, and is nicer to work with for smaller-scale applications.
Creating a Custom Dispatcher Structure
Assuming your application follows the typical Elixir structure, we need to define a custom dispatcher structure for the Cowboy child spec in your supervision tree. This dispatcher structure is used for designating a separate module for handling any WebSocket requests we get, keeping that handling separate from any HTTP routes.
In this minimal example, we define a dispatcher structure that routes requests starting with "/websocket" to the SocketRouter
module, while Router
handles all other paths. Since the order is hierarchical, the WebSocket route comes first, followed by the :_
catch-all atom for any others.
Stub HTTP Router
For the purpose of this writeup, we'll use a super minimal HTTP router module that simply responds with "Hello, world" to any request:
WebSocket Handler
Here, we specify that the module conforms to the :cowboy_websocket_handler
behaviour using @behaviour
. This consists of a few callbacks:
init/2
initializes the WebSocket handler and specifies any options (in this case, just overriding the defaultidle_timeout
andmax_frame_size
).websocket_init/1
initializes the specific WebSocket connection and sets up the initial state.websocket_handle/3
handles specific WebSocket frames, such as responding to:ping
frames with a:text
response of "pong".websocket_info/3
handles any Elixir messages recieved from other parts of the application, and forwards them to the WebSocket client.terminate/3
is called upon termination and allows performing cleanup tasks such as terminating other processes.
Profit (?)
With the demo app running locally, we can see these endpoints in action! First up, the HTTP router:
And next, the WebSocket router:
Once connected, sending a text frame of "ping" yields a response of "pong", as expected:
So there you have it! I hope this quick overview is useful; I've personally loved working with Cowboy so far and am glad I was able to make it work for this use-case of mine š
More resources
The full docs for cowboy_websocket
are worth a read: