Gigalixir "elixir release" not generating correct URLs

In a normal Phoenix app. the Router.Helpers generate correct urls, e.g.

Routes.user_confirmation_url(conn, :confirm, token)

will output https://myapp.com/user/confirm/?token=token.

When I deploy an app to Gigalixir using Elixir releases, I have to specify (as per docs https://gigalixir.readthedocs.io/en/latest/modify-app/releases.html#modifying-existing-app-with-elixir-releases):

url: [host: nil, port: 443]

in config/releases.exs.

And this results in incorrect URLs being generated (they come out as https://localhost:443/user/confirm/?token-token).

I can workaround this for now, by setting a new ENV var, say, BASE_PATH=https://myapp.com and generating URLs like this:

Routes.user_confirmation_url(%URI{path: System.get_env("BASE_PATH"), :confirm, token)

but I don’t like having to repeat this code in many places.

So I have 2 questions:

  1. Is there some config I can change to make Elixir releases on Gigalixir use correct base path for URLs?
  2. Is there some way to “hijack” the URL-generating function that will let me replace “localhost” string?

Thanks in advance.

My current workaround, for anybody else with the same problem.

Make a Gigalixir configuration variable BASE_PATH with value like https://app.staging-myapp.com.

Then make a module (to keep the “hack” logic in one place):

defmodule MyApp.UrlGeneration do
  @moduledoc """
  A "hacky" module to assist with generating correct URLs on Gigalixir "Elixir" releases.

  See here for more info:

  https://elixirforum.com/t/gigalixir-elixir-release-not-generating-correct-urls/35586
  """

  def uri, do: %URI{path: System.get_env("BASE_PATH", "http://localhost:4000")}
end

Then change any URL generating code like this:

Routes.user_confirmation_url(conn, :confirm, token)

into this:

Routes.user_confirmation_url(MyApp.UrlGeneration.uri(), :confirm, token)

I don’t have experience with Gigalixir, but I have ran into this problem before. The URL generation code includes two variations: *_url and _path. The *_url one that you are using requires the host url to be specificied in the config/releases.exs file. The *_path variation does not.

Quoted from https://hexdocs.pm/phoenix/routing.html#path-helpers and https://hexdocs.pm/phoenix/routing.html#more-on-path-helpers:

Path helpers are functions which are dynamically defined on the Router.Helpers module for an individual application. For us, that is HelloWeb.Router.Helpers. Their name of each path helper is derived from the name of the controller used in the route definition. Our controller is HelloWeb.PageController, and page_path is the function which will return the path to the root of our application.

That’s a mouthful. Let’s see it in action. Run iex -S mix at the root of the project. When we call the page_path function on our router helpers with the Endpoint or connection and action as arguments, it returns the path to us.

iex> HelloWeb.Router.Helpers.page_path(HelloWeb.Endpoint, :index)`
"/"

What if we need a full url instead of a path? Just replace _path with _url:

iex> Routes.user_url(Endpoint, :index)
"http://localhost:4000/users"

The _url functions will get the host, port, proxy port, and SSL information needed to construct the full URL from the configuration parameters set for each environment. We’ll talk about configuration in more detail in its own guide. For now, you can take a look at config/dev.exs file in your own project to see those values.

TL;DR try using Routes.user_confirmation_path(conn, :confirm, token) instead of Routes.user_confirmation_url(conn, :confirm, token).

I presume you are using this as an email sent in a link for user email confirmation. If that’s the case, I’d recommend prepending the BASE_PATH that you have set up to the path. I’d say that’s still cleaner than leveraging %URI{} and overriding the path key.

You may need System.get_env("BASE_PATH") <> Routes.user_confirmation_path(conn, :confirm, token)

I’m not sure why the docs say to set that to nil. I looked at an old app i deployed to gigalixir and this is what I had

config :pipsqueak, PipsqueakWeb.Endpoint,
  server: true,
  # Needed for Phoenix 1.2 and 1.4. Doesn't hurt for 1.3.
  http: [port: {:system, "PORT"}],
  url: [host: System.get_env("APP_NAME") <> ".gigalixirapp.com", port: 443]

Using _path provides correct relative path but without the domain part.

I don’t like your suggestion of prepending System.get_env("BASE_PATH") everywhere I need it.

I would rather have a dedicated module that acts as a single source of “realisation” for what is really a hack. It makes future modification and understanding easier, especially for posterity (other devs).

This is good to know but I wonder

  1. How “old” is this “old app” you mention?
  2. Does this help for a gigalixir app using a custom domain?

This is because the config is not the only place do derive the domain from, but it might just be wrong in telling you to set it to nil. It likely should just skip the :host key completely.

The conn does hold the domain the app is accessed by, which is then used by the helper to build up the full url. This is especially useful for places like gigalixir, where it’s likely that the app is accessable via multiple domains. What this will prevent however is using MyApp.Endpoint with those helpers to create full urls.

There is a place, which needs an explicit host: nil, which is for force_ssl:

1 Like

To summarise, the host config should be either nil or omitted.

This means the route helpers won’t generate correct URLs so I will keep my current workaround.

If you’re using a custom domain, I would just hard code the domain in the config.

I don’t understand why they are asking to set it to nil, perhaps you should e-mail support and ask for clarifications.

The thing is most apps are running behind a proxy, Gigalixir is no different, and inside the proxy, you are typically running on localhost. So here is how it works:

[browser] -- app.gigalixir.com --> [proxy] -- localhost --> [yourapp]

So if you don’t configure the host, the app is correct in thinking it is running on localhost. We could use the X_FORWARDED_FOR headers to figure out the actual host but that has security implications when read and it is not behind a proxy. So my suggestion is to explicitly set the :host to your actual host instead of nil in your config files. You can either hardcode it or use an environment variable.

TL;DR: don’t use nil, set it to your actual host.

2 Likes

The same app is deployed on different domains (using different Gigalixir accounts).

Because “paid” Gigalixir accounts cannot have “free tier” apps, I have 2 Gigalixir accounts - one paid account for live apps and one free account for “staging” versions of the same apps.

And what about omitting :host entirely? That seems to be working for me now but I don’t know about these “security implications”.

Do you think it is OK to omit the :host variable?

Set an env var on each instance for their respective host name and then you can set it as

config :pipsqueak, PipsqueakWeb.Endpoint,
  server: true,
  # Needed for Phoenix 1.2 and 1.4. Doesn't hurt for 1.3.
  http: [port: {:system, "PORT"}],
  url: [host: System.get_env("HOST"), port: 443]

Yes this makes sense now, if I believe that it’s OK to set the host like that.

I am not yet 100% comfortable ignoring the docs, so I am omitting host for the time being.