Matching all subdomains in Phoenix routing

I love how one can define a host where the last bit would automatically get matched:

scope "/", host: "about.mysite." do`

But it’s impossible to do the opposite end:

scope "/", host: ".mysite.com" do

(for matching all remaining subdomains)

What are the the implications that prevent this from being possible and what are the alternatives for implementing wildcard subdomain matching in a Phoenix project?


Out of curiosity found that this logic is defined in Plug.Router.Utils.build_host_match:

def build_host_match(host) do
  cond do
    is_nil(host) -> quote do: _
    String.last(host) == "." -> quote do: unquote(host) <> _
    is_binary(host) -> host
  end
end

Therefore I naively tried adding this matching rule :sweat_smile::

      String.first(host) == "." -> quote do: _ <> unquote(host)

But it’s not that easy, since it results in:

Compiling 1 file (.ex)

error: a binary field without size is only allowed at the end of a binary pattern, at the right side of binary concatenation and never allowed in binary generators. The following examples are invalid:

    rest <> "foo"
    <<rest::binary, "foo">>

    "foo" <> rest
    <<"foo", rest::binary>>


  lib/my_app_web/router.ex:1

In short, the “match any subdomain” clause in the router is to just not specify the :host option. The subdomain itself will be available in conn.host so you could pull it out in a plug and do whatever you need to do with it.

If you want different routes depending on the presence of a subdomain, that’s a little different. There are a few discussions around here about it but I’m having trouble finding the ones I found helpful. I do have the (abandon) project I was using it in, though. In short you want to make a plug, check if there is a subdomain then delegate to another router. Maybe there is an easier way at this point? In any event, here is my code:

# lib/my_app_web/subdomain_router.ex
defmodule MyAppWeb.SubdomainRouter do
  # This is just a regular router but with subdomain specific routes
end
# lib/my_app_web/plugs/subdomain_plug
defmodule MyAppWeb.SubdomainPlug do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, router) do
    case get_subdomain(conn.host) do
      subdomain when byte_size(subdomain) > 0 ->
        conn
        |> fetch_session()
        |> put_session(:subdomain, subdomain)
        |> router.call(router.init({}))
        |> halt()
      _ -> conn
    end
  end

  defp get_subdomain(host) do
    root_host = MyAppWeb.Endpoint.config(:url)[:host]

    if root_host == "localhost" do
      # This handles tests
      ""
    else
      String.replace(host, ~r/.?#{root_host}/, "")
    end
  end
end
# BOTTOM of lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
  # ...
  plug MyAppWeb.SubdomainPlug, MyAppWeb.SubdomainRouter
  plug MyAppWeb.Router
end

Maybe it’s easier at this point? I did always find this to be a little bit of a pain point in Phoenix. Actually, have you tried simply host: "." at all? I can’t remember if that’s a thing or not and don’t feel like spinning up that app :sweat_smile:

3 Likes