Mixing WebSockets & HTTP endpoints in Elixir with Cowboy + Plug
July 20, 2023
5 min read
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:
Here, we specify that the module conforms to the
:cowboy_websocket_handler behaviour using
@behaviour. This consists of a few callbacks:
init/2initializes the WebSocket handler and specifies any options (in this case, just overriding the default
websocket_init/1initializes the specific WebSocket connection and sets up the initial state.
websocket_handle/3handles specific WebSocket frames, such as responding to
:pingframes with a
:textresponse of "pong".
websocket_info/3handles any Elixir messages recieved from other parts of the application, and forwards them to the WebSocket client.
terminate/3is called upon termination and allows performing cleanup tasks such as terminating other processes.
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 😁
The full docs for
cowboy_websocket are worth a read: