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
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…
thanks! I’ll look at doing this tonight or sometime in the morning.
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.
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.
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.