Skip to content

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.

application.ex

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:

router.ex

WebSocket Handler

ws_router.ex

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 default idle_timeout and max_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: