Adding a match_path function like in Plug

phoenix
router

#1

Hi!

There is currently no easy way in Phoenix to retrieve the path of the route against which a request was matched. For example, if you define a get "/users/:id" route and you receive a GET /users/1 request, you have no easy solution to retrieve that the route was /users/:id. You’d only have access to conn.request_path which would be something like /users/1 in such case.

Accessing the route can be really useful in the following cases:

  • logging - logging your requests by adding a route metadata in order to filter quite easily by route.
  • metrics - tagging your request metrics with the route so that you could for example check the performance of your app per route. At my company, we are currently tagging our metrics by the request_path but it means that each id will create a different “metric/graph”. Given that our metrics aggregator pricing increases with the cardinality of the metrics, you can easily see how that could create some issues.
  • rate_limiting - creating a route-based rate limiting.

This issue has been brought up a few time on the forum (Get match path in Phoenix, Access matched Route from phoenix Conn) and since a similar functionality has been merged into Plug (https://github.com/elixir-plug/plug/pull/630), I think it would be great to have such a feature in Phoenix as well. Please let me know if I’m mistaken here.

I quickly experimented locally and I think the following change in the Router would be enough to solve this:

  defp build_match({route, exprs}, known_pipelines) do
-   %{pipe_through: pipe_through} = route
+   %{pipe_through: pipe_through, path: route_path} = route

    %{
      prepare: prepare,
      dispatch: dispatch,
      verb_match: verb_match,
      path: path,
      host: host
    } = exprs

    {pipe_name, pipe_definition, known_pipelines} =
      case known_pipelines do
        %{^pipe_through => name} ->
          {name, :ok, known_pipelines}

        %{} ->
          name = :"__pipe_through#{map_size(known_pipelines)}__"
          {name, build_pipes(name, pipe_through), Map.put(known_pipelines, pipe_through, name)}
      end

    quoted =
      quote line: route.line do
        unquote(pipe_definition)

        @doc false
        def __match_route__(var!(conn), unquote(verb_match), unquote(path), unquote(host)) do
+         var!(conn) = update_in var!(conn).private,
+           &Map.put(&1, :phoenix_route, unquote(route_path))

          {unquote(prepare), &unquote(Macro.var(pipe_name, __MODULE__))/1, unquote(dispatch)}
        end
      end

    {quoted, known_pipelines}
  end

and then add a function to the Router:

defmodule Phoenix.Router do
  @doc """
  Returns the path of the route that the request was matched to.
  """
  @spec match_path(Plug.Conn.t) :: String.t | nil
  def match_path(%Plug.Conn{} = conn) do
    Map.fetch(conn.private, :phoenix_route)
  end
end

path = Phoenix.Router.match_path(conn)

I’d be willing to open a PR for that but since I’m not too familiar with Phoenix source code, I’d love to get feedback on whether you feel that change should go into Phoenix and whether the change I suggested above makes sense or not.

Thanks!