Are cascading routers possible?

Is it possible to do a forward directly to a router? I see in Google searches that cascading routers were in the plans a few years ago, but I don’t see any recent examples. I like my HTTP interface to live with its implementation.

Not sure if this is what you’re asking about, but there’s Plug.Router.forward/2.

Yes, and I am using the forward example from here currently:

https://hexdocs.pm/phoenix/routing.html

The only quibble to me is that it suggests forwarding to another plug rather than to a router directly, which feels like an extra level of indirection. I want it to be as easy as including routes for subcomponents in a Django app is.

At first I tried something like what Chris McCord mentioned in 2105 might become possible in the future:

https://groups.google.com/d/msg/phoenix-talk/FoQ4l5GmlY8/BVYHEcp13JQJ

…but I couldn’t get it to work and since I don’t fully understand the macros and routing system, I wasn’t sure if it’s just not possible or it’s my mistake.

Routers are plugs, so if you can forward to a plug you can forward to a router.

2 Likes

Ok, and I had seen that he said that in the thread I linked. He also said “we don’t have full support for cascading routes (yet)”. So I guess my next question is “does Phoenix have full support for cascading routes yet?”

Can you elaborate on what “full support for cascading routes” means?

Maybe not, because I don’t fully understand how Phoenix’s macros and routing system interacts. Do you have any guesses what he meant by it?

I guess the problem is the typical chicken-and-egg one for newbies: I don’t know enough to ask proper questions.

I’m used to Django where it’s trivial to concatenate routes from components. What I want to do in Phoenix is forward to a router in a component and continue to use Phoenix macros like in the parent router. That seems like the base case. But in the example I linked to in the Phoenix documentation, they forward to an intermediate plug and use plug macros instead of Phoenix ones in the component router.

From Chris’s messages I’d expect that he’s talking about plugging multiple routers in succession in the endpoint and having the first one not throw a 404 if it does not match. So you don’t need the “single entry router” which forwards to the other router modules.

You can do a lot with plugs, they are very flexible and composable.

The Phoenix router is more opinionated, and does more for you. One of the features of
Phoenix is that the router uses macros and function head pattern matching to handle routes at compile time, not run time, so they are very fast.

Sometimes you need more flexibility, e.g. to route based on characteristics of the request.

scope "/", Bounce.Api do
  get "/*path", GlobRouter, []
  post "/*path", GlobRouter, []
end

defmodule Foo.Api.GlobRouter do
  def init(opts), do: opts

  def call(%Plug.Conn{request_path: path, params: params} = conn, _opts) do
    cond do
      # API request to "/" instead of /api. Technically not correct, but we still accept it
      path == "/" and Map.has_key?(params, "apikey") ->
        conn
        |> to(Foo.Api.Controller, :api)
      # Request with UUID, e.g. /793f8f9e-d2f4-11e6-9c63-a2dd21000e5a
      Regex.match?(~r/^\/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}/, path) ->
        to(conn, Foo.Api.Controller, :redirect)
      true ->
        to(conn, Foo.Api.Controller, :invalid)
        # raise Phoenix.Router.NoRouteError, conn: conn, router: Router
    end
  end

  defp to(conn, controller, action) do
    controller.call(conn, controller.init(action))
  end
end

You can also route using Plug.Builder. It lets you include a chain of plugs, followed by your own. You can use it to do anything.

defmodule Foo.Api.RouteRequest do
  use Plug.Builder

  plug Foo.Api.RateLimit
  plug :route_request

  def route_request(%{assigns: %{foo: "A"}} = conn, _opts) do
    Foo.Api.A.handle(conn)
  end
  def route_request(%{assigns: %{foo: "D"}} = conn, _opts) do
    Foo.Api.Direct.call(conn, Foo.Api.Direct.init([]))    
  end
end
2 Likes