Umbrella with 2 phoenix apps, how to forward request from 1 to 2 and vice versa

phoenix
Tags: #<Tag:0x00007fcf97501ea0>

#1

Reading https://hexdocs.pm/phoenix/Phoenix.Router.html#forward/4 forwarding to another endpoint is no advisable.
How should I do this? Can I use one port for both apps (and only start one phoenix.server)? An example project on github maybe?


#2

This might be relevant:


#3

Thanks, but that looks like a strange hack. Anyone else? Seems others are interested also seeing the click-counter on sheharyarn’s link.
What I want is a menu-item in both app.html.eex files that links to the main page of the other app.


#4

How about 3 apps? 2 phoenix apps and a third to handle the logic. The two phoenix app would both use the third to handle the computation that you’d otherwise be forwarding from one phoenix app to the other.


#5

I have a solution. If you have a better one I’d like to hear.
In app.html.eex:

<li><a href="<%= @link_to_admin %>">Admin</a></li>

All the apps under the umbrella use different ports. This is within my :bpm_server app, the link is to :admin. bpm_server has :admin as a dep

In a controller for authentication within :bpm_server I have

admin_port_plus_slash = Admin.Router.Helpers.page_url(Admin.Endpoint, :index) |> String.split(":") |> Enum.at(2)
conn.assigns[:link_to_admin]
conn = assign(conn, :link_to_admin, “http://” <> conn.host <> “:” <> admin_port_plus_slash <> “admin”)


#6

You surround it in codefences (with an optional syntax highlighter too) like:
```{optionalSyntaxHighlighter, or leave blank}
Pure text
```

Thus doing this:
```elixir
def blah, do: nil
```
Will end up like:

def blah, do: nil

It is standard markdown syntax used by github and others if you are interested in looking up more features. :slight_smile:


#7

Thanks, I’ll wire a knot somewherein.


#8

Hi Stefan, maybe it’s just me but why do you care about ports?

Your production server is not going to publicly expose these ports and you will have a reverse proxy for your umbrella app so I don’t think the admin_port_plus_slash is needed in your code.

In order to get this working on my dev env (laptop) I use https://caddyserver.com with virtual hosts and reverse proxy. (https://caddyserver.com/docs/proxy)


#9

Two phoenix apps need to listen on two distinct ports. If you’re trying to incorporate an admin site for your main app, you could consider one app with one endpoint and special handling based on the hostname used to access the site.

You could make a helper plug like:

defmodule Host do
  @behaviour Plug

  def init(hosts), do: hosts

  def call(conn, hosts) do
    plug_mod = Map.fetch!(hosts, conn.host)
    plug_mod.call(conn, plug_mod.init(nil))
  end
end

And then immediately in your endpoint you could do the switch:

plug Host, %{
  "mysite.com" => SitePlug,
  "admin.mysite.com" => AdminPlug
}

You’d need to move endpoint plugs to the plug modules which would be based on Plug.Builder

Standard disclaimers apply :slight_smile:


#10

Here is another approach. Check out the master_proxy app.

The video of the related talk is here.


What is benefit of having multiple phoenix endpoints
#11

Thanks Sasa, I’m just starting as webdeveloper (as a freelancer, learning at home). I don’t have an own domain name yet, so I use ip-adresses. I will try to make your solution working as soon as I have a domain name.


#12

modify your /etc/hosts file and you can have your own domain name as far as that one machine is concerned


#13

For free? :slight_smile: Nice, thanks.


#14

I do not understand completely how I could use this solution. Are there advantages compared to the following one?
Maybe I’m missing some things (I for example don’t know how the config works with domain names usable from another machine, maybe that makes following unusable for “real” domain names).

I edited etc/hosts (this is in windows), as I do not have a domain name (suggestion from Greg):

127.65.43.21 www.bpmserver.com
127.65.43.22 www.admin.bpmserver.com

Then I forwarded ports (also windows):

netsh interface portproxy add v4tov4 listenport=80 listenaddress=127.65.43.21 connectport=4000 connectaddress=192.168.1.106

netsh interface portproxy add v4tov4 listenport=80 listenaddress=127.65.43.22 connectport=4001 connectaddress=192.168.1.106

192.168.1.106 is the static address of my ubuntu vm btw.

As mentioned in a previous post I have the menu-item in app.html.eex from where I want to reach the :admin app from the :bpm_server app.

<li><a href="<%= @link_to_admin %>">Admin</a></li>

In the controller for authentication I change the previously mentioned code to:

conn.assigns[:link_to_admin]
conn = assign(conn, :link_to_admin, "http://www.admin.bpmserver.com/admin") 

The url could be an env var of course. The solutions works btw.


#15

From what I can tell, you have two endpoints which listen on different ports. As I explained here, that has a nice benefit that you can restrict the access to the admin part of the site by simply not exposing the port of the admin site.

In contrast, the solution I mentioned here listens on a single port, so this means you need to have some other auth mechanism in the admin site, since anyone can access it.

Other than that, I’m not completely sure which approach is better. I have a feeling you can do whatever you want with either approach. I guess having just one endpoint means less things to configure, so I’d probably try that approach first.


#16

I have used you example with master_proxy app with success. But it does not work for web sockets. I can not figure out how to forward the socket request to the corresponding phoenix app.


#17

I found myself in similar situation. My GraphQl subscriptions can’t get through master_proxy. Did you eventually find any solution for forwarding sockets?


#18

I have not found yet how to forward from dispatch directly but it works from the plug.
This is my working solution at the moment:

defmodule MasterProxy.Application do
  @moduledoc false
  use Application

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

    port = (System.get_env("PORT") || "8080") |> String.to_integer()
    websocket = {Phoenix.Transports.WebSocket, {Carts.ApiWeb.Endpoint, Carts.ApiWeb.UserSocket, :websocket}}

    cowboy_options = [
      port: port,
     dispatch: [
        {:_,
          [
            {"/socket/websocket", Phoenix.Endpoint.CowboyWebSocket, websocket},
            {:_, Plug.Adapters.Cowboy.Handler, {MasterProxy.Plug, []}}
          ]}
       ]
  ]

  cowboy = Plug.Adapters.Cowboy.child_spec(:http, MasterProxy.Plug, [], cowboy_options)

   children = [
    cowboy
   ]

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

and the plug:

  defmodule MasterProxy.Plug do
    @moduledoc false
    @hosts %{
      "carts-api" => Carts.ApiWeb.Endpoint,
      "loyalty-api" => LoyaltyOld.Api.Endpoint,
      "restaurants-api5" => Restaurants.Api.Endpoint,
      "restaurants" => Restaurants.Api.Endpoint
    }

    def init(options) do
      options
    end

    def call(conn, _opts) do
      subdomain = conn.host |> String.split(".", parts: 2) |> List.first()

      case Map.fetch(@hosts, subdomain) do
        {:ok, plug_mod} -> plug_mod.call(conn, plug_mod.init(nil))
        :error -> raise "Host not allowed #{conn.host}"
      end
    end
  end