Porting over a rails service. How to handle existing requests with .json suffix in phoenix

I’m currently working on porting a rails service over to phoenix (this is fantastic because I’m liking elixir and phoenix and didn’t really drink the rails cool-aid). One issue I’m currently looking to face in a somewhat acceptable way is existing requests being made to the rails service with a .json suffix (something like www.something/some_resource/5.json. I’m wanting to be able to just have resource in my phoenix router file and not have n number of additional .json custom routes and not have some duct tape to remove the .json from some identifier in the url. I know there could potentially be many ways of accomplishing this but looking for the best way to handle these and to slide this in so none of the existing clients notice any change (other than better performance). Any assistance or input would be greatly appreciated.

Just to clarify, do you need to support full on “content negotiation” where 5.json renders a JSON response whereas 5.html renders an HTML response?

A rewrite rule to “transform” x.json to x?_format=json? (rewrite ^(.+)\.(json)$ $1?_format=$2 last;)

I’d also have liked a Phoenix equivalent to .:format

1 Like

it is kind of silly. It is just a json api and both the rails and phoenix version are just returning json but a lot of requests have the .json suffix

I’m still learning some unknown unknowns in phoenix and elixir. Where would this be done and is there a part in the docs I should refer to?

I was hoping if I did something like this though, that it was done before all of the params magic happened so I wouldn’t have to do something like String.replace(string, ~r/.json$/, "") in every controller that has some url param. I think a plug would work but wondering if there isn’t something better that I’m not considering.

Also, I’m not sure if there isn’t some way around having to make the extra custom urls in the router file after the normal resource that handles the normal requests but that would be awesome if there wasn’t some way to handle those as well

think you’ll want to look at using a Plug

I’m no expert, but maybe something like:

defmodule MyApp.Plugs.RewriteJson do
  import Plug.Conn

  def init(default), do: default

  def call(%Plug.Conn{request_path: request_path, path_info: path_info} = conn, _) do
    case String.ends_with?(request_path, ".json") do
       false -> conn
       true ->
           #awful line defp a helper function..
           %{conn | :path_info => List.update_at(path_info, length(path_info)-1, &(String.replace_suffix(&1, ".json", "")))}
    end
  end
end

since this is a rewrite I think you need to add the Plug in your Endpoint, before the Plug.Parsers

plug(MyApp.Plugs.RewriteJson)

you might need to add json headers dependent on the client not sending those…

3 Likes

thanks! I’ll look at doing this tonight or sometime in the morning.

6 Likes

This works! Thanks! I did notice it hasn’t been updated in a bit but still works with the latest elixir and phoenix versions so I guess there hasn’t been a need to make changes to it.

2 Likes

Yep I use it (well actually I copied it into my project because I needed to make an edit). Old doesn’t mean not modern, just means it’s Done. :slight_smile:

Here’s my copy of it’s file with the change I needed (to enforce only certain suffix changes, I needed .'s to exist if it wasn’t one of the known types):

defmodule TrailingFormatPlug do
  @behaviour Plug

  def init(options), do: options

  def call(%{path_info: []} = conn, _opts), do: conn
  def call(conn, opts) do
    path = conn.path_info |> List.last() |> String.split(".") |> Enum.reverse()

    case path do
      [_] ->
        conn

      [format | fragments] ->
        opts[:valid]
        |> case do
          [] -> true
          nil -> true
          formats when is_list(formats) -> Enum.member?(formats, format)
        end
        |> if do
          new_path       = fragments |> Enum.reverse() |> Enum.join(".")
          path_fragments = List.replace_at conn.path_info, -1, new_path
          params         =
            Plug.Conn.fetch_query_params(conn).params
            |> update_params(new_path, format)
            |> Map.put("_format", format)

          %{
            conn |
            path_info: path_fragments,
            query_params: params,
            params: params
          }
        else
          conn
        end
    end
  end

  defp update_params(params, new_path, format) do
    wildcard = Enum.find params, fn {_, v} -> v == "#{new_path}.#{format}" end

    case wildcard do
      {key, _} ->
        Map.put(params, key, new_path)

      _ ->
        params
    end
  end
end

The difference in mine is you can optionally pass a valid: ["json", "html", "xlsx"] or whatever things you want it to accept. If :valid is specific then it will only handle those, else it will not change it at all (so /blah.blorp will go through as normal). I meant to get around adding an MFA call on it as well for :valid but never did as I didn’t need it, but would be useful for a dynamic comparisons.

3 Likes