Phoenix and the Trailing Format Plug
Many frameworks use the Accept
header to determine what type of content to send. For API’s you’re often expected to set a header such as Accept:application/json
to indicate that you want a response in JSON format. But what if you’re re-writing an API where clients expect to specify the format as an ‘extension’ such as http://example.com/api/tasks/1.json ?
Let’s set up a simple example and see what happens in Phoenix.
Generate Phoenix App and API
Step 1: Generate the usual Phoenix example app:
Step 2: Generate a simple JSON API for some tasks:
Step 3: Edit web/router.ex
In this case we need to un-comment the api scope and add an extra line:
Step 4: Create and Migrate the database
Add a Task
Let’s start the server inside iex
so we can add a task directly to the database as described in Ecto Models.
At the iex prompt, we’ll use alias
to shorten the commands we must type, and then create a changeset and insert it in the repo.
Now we have a task in the database, and we can see it has the id
of 1
.
Examine Routes
In a separate console window (be sure to leave the app running, we’ll need it in a moment,) we can see what routes are available:
Use API
That means we should be able to GET http://localhost:4000/api/tasks/1, right? Let’s try it, either with curl
or in a browser:
Hmm… that’s not what I expected. I should see my Very Important Task and its due date!
I puzzled over this for a while, and finally figured out that while the fields you specify in mix phoenix.gen.json
get added to the model, they do NOT get added to the view.
To fix this, we need to add those fields to the view in web/views/task_view.ex
Now let’s try that again. Either with curl or in a browser, request http://localhost:4000/api/tasks/1
That’s better!
Legacy Clients
But what about those legacy clients who insist on appending the format as an extension? If you try…
… you get a bunch of html – a text version of the lovely purple error page I’m sure you’ve seen before. The error is:
The error is coming from line 27 in task_controller.ex
:
It’s trying to use “1.json” as the ‘id’ to find a record in the database, which is causing an error.
This was the topic of a question on phoenix-talk the other morning that I started to research, but didn’t have time to finish. I had gotten as far as:
By adding a route and inspecting
conn
in the controller (just playing with the generated Phoenix app) I can see that given a simple GET with
curl http://localhost:4000/api/v1.0/resource/1.json
there’s a
path_info: ["api", "v1.0", "resource", "1.json"]
that you might be able to use. And then instead of the
:accepts
plug, which seems to be working off the Accepts header, you might have an:extension
plug of your own that figures out what they’re requesting.
Shortly thereafter, Chris McCord said:
What you are after is the trailing_format_plug
Trailing Format Plug
So! As usual, someone else has already solved the problem I have. Let’s have a look at this trailing_format_plug and see how to use it.
A quick look at the README shows it is MIT licensed, so we’re good there, however the instructions are somewhat sparse. We’re meant to add the trailing_format_plug dependency to mix.exs, which is easy enough:
Since we’ve modified the dependencies, let’s make sure everything is present locally, and then commit the changes to mix.exs and mix.lock:
Then, since we are using Phoenix, the docs say “Add the plug to the :before pipeline in your router.ex”.
Well, we don’t have a :before pipeline, and adding one didn’t seem to work. (I later found out it’s something that has been removed from the framework in favor of endpoints.)
After trying several things I asked for help in #elixir-lang on Freenode, and it turns out that the usage instructions are not correct for the lastest version of Phoenix. For the plug to work as-is, it must be placed in the Endpoint.
Let’s make this change to lib\my_app_802337\endpoint.ex
Now we’re back in business, and those legacy clients can make their requests with a trailing format ‘extension’:
However this has broken other routes – you can no longer visit http://localhost:4000 for example. As Chris explained (and patched) on irc, it would be better to fix the plug so that it can work in a pipeline in router.ex and does not have to be run on every request.
Patch
I’ve forked the plugin and branched to apply Chris’ patch:
https://github.com/wsmoak/trailing_format_plug/tree/phoenix_0_15_update
To use this version in our example app, we can update mix.exs to point at that branch on GitHub:
Then run mix deps.get
and we should see it cloning the code locally:
And then move the plug (again!) from lib/my_app_802337/endpoint.ex
back to web/router.ex
, this time in the :api
pipeline:
Now the only time the TrailingFormatPlug will be used is when a request comes in that matches the /api
path, so it’s no longer breaking the other routes.
I can see that http://localhost:4000/api/tasks/1.json works again, but with all our changes and without tests I’m not really sure. A quick way to find out is to comment out the plug TrailingFormatPlug
line in web/router.ex
(add a #
in front) and confirm that you get the error again.
Next up is to make sure the patched version of the plug works with all the routes we saw earlier with mix phoenix.routes
, write some tests for the new behavior, and submit a PR.
Conclusion
We’ve learned that while there are lots of resources available in the community, they’re not all kept up to date with the latest changes in other resources! My guess is that the author of this plug is not using Phoenix, or hasn’t upgraded lately, and so hasn’t noticed the problem.
We’ve also seen how to use a patched version of a dependency by pointing at a branch on GitHub. (If you need to work with it locally, add the dependency as {:trailing_format_plug, path: "../path/to"}
.)
The code for this example is available at https://github.com/wsmoak/my_app_802337/tree/20150803 and is licensed MIT.
Copyright 2015 Wendy Smoak - This article first appeared on http://wsmoak.github.io and is licensed CC BY-NC.