How do Plug.Conns get to the socket macro in Endpoint.ex?

I built a plug that sits on top of several Phoenix app Endpoints. This plug routes the connection to the appropriate Phoenix Endpoint by pattern matching on the conn’s host value, it then does an WhicheverApp.Endpoint.call(conn, opts) to forward the connection along to the right app.

The problem I’m having is that for a websocket connection, the conn is not being caught by the socket “/socket” WhicheveApp.UserSocket macro. Even though this line is part of the Endpoint module, I’m not sure if it actually part of the plug pipeline.

Is there a way this can be done? Does anyone know how the socket macro fits within the Endpoint?

Edit: I guess I could make my custom plug router a phoenix app to make use of however the Endpoint is currently used. I could remove all the duplicate plugs in this “Router” Endpoint so that they are not called twice, and have the socket macro up top catch websocket requests for all the apps at this level and then have some websocket router that then reroutes the websocket request to the appropriate app’s socket. I am still curious how the conn initially hits the Endpoint to begin with and how it is different than Endpoint.call

1 Like

It is processed at the ‘socket’ line in the endpoint as far as I recall. Any plugs for sockets need to be before the socket line I’d guess as the socket is the end point for sockets (a pass-through for everything else).

2 Likes

Right, but it doesn’t seem like the socket line is actually a part of the plug pipeline in the Endpoint, because calling Endpoint.call on a conn will send that conn through the Endpoint pipeline. But when that is done for conn requesting the /socket path (or whatever you choose to call it), the socket macro is skipped and instead that request for a websocket is being forwarded to the router plug

1 Like

Hmm, entirely possible, let’s look.

First we see:

So it is just setting the sockets into a module attribute of :phoenix_sockets, which it registered at:

That appears to only be used at:

Which is only called from:

Which that function gathers up the sockets and combines it with the endpoint itself at:

Of which cowboy then listens for them distinctly.

I.E., the socket and endpoint plugs are entirely differently lines, in this case I’m unsure how to stuff a plug into the socket path, may need to wait for someone knowledgeable in this such as one of the devs like @josevalim or @chrismccord to appear. :slight_smile:

4 Likes

Thanks for that great breakdown. It looks like you could add websocket rules to the Plug.Cowboy.Adapater.child_spec via the dispatch option and have any websocket requests get caught and routed from there.

2 Likes

In case anyone is interested, here is how I got it to work (thanks to OvermindDL1’s helpful breakdown)

defmodule CustomRouter do
  use Application

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    myapp_websocket = {Phoenix.Transports.WebSocket, {MyApp.Endpoint, MyApp.UserSocket, :websocket}}
    cowboy_options = [
      port: 4001,
      dispatch: [
        {:_, [
          {"/socket/websocket", Phoenix.Endpoint.CowboyWebSocket, myapp_websocket},
          {:_, Plug.Adapters.Cowboy.Handler, {CustomRouter.Endpoint, []}}
        ]}
      ]
    ]

    children = [
      Plug.Adapters.Cowboy.child_spec(:http, CustomRouter.Endpoint, [], cowboy_options)
    ]

    opts = [strategy: :one_for_one, name: CustomRouter.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

I leaned heavily on the examples found in the Phoenix.EndpointCowboyHandler docs to make this work. This was very interesting to do at such a relatively low level. I think in the future I will rewrite this to use more of Phoenix’s abstraction. Instead of having a CustomRouter.Endpoint, which in this case is just a module plug that pattern matches on a host and then calls the appropriate Phoenix App’s Endpoint, I will make an outer Phoenix App that has nothing but an Endpoint which does the same thing as my custom plug router (just one plug that pattern matches), but also uses the socket macros to more cleanly route websocket requests to the appropriate Socket module, which per OvermindDL1’s breakdown, has shown me it will basically compile to the same exact thing I’ve done here.

Edit: actually, it looks like you can pass the host as an option to the scope in the Phoenix router!

3 Likes

That is awesome!

Any thoughts of documenting up the process and submitting a PR to the Phoenix project itself? Maybe even make the process more simple via some new Phoenix functions to handle it so as to not need to go as low-level? :slight_smile:

2 Likes

What I understand from the above snippet is that we route any websocket request, no matter the host to the same socket.
What if there are different sockets on each Phoenix app? How do we route “/socket/websocket” to MyApp1.Endpoint or to MyApp2.Endpoint based on the host?

1 Like

Ultimately, I just opted to use different paths for routing to my individual sockets but looking at the cowboy docs, it looks like you can match on the host in the first element of the tuple inside the dispatch list. In the example above, :_ is used and acts as a catch-all for all hosts:

dispatch: [
    {:_, [
      {"/socket/websocket", Phoenix.Endpoint.CowboyWebSocket, myapp_websocket},
      {:_, Plug.Adapters.Cowboy.Handler, {CustomRouter.Endpoint, []}}
    ]}
  ]

https://ninenines.eu/docs/en/cowboy/1.0/guide/routing/ (Particulalry the section titled Compilation)