Scrubbing GET Params With Phoenix

Nicolas Bettenburg bio photo By Nicolas Bettenburg

Introduction

This article is written for Elixir 1.2.5, Phoenix 1.1.4, and Ecto 1.1.7. Please note that future versions of Elixir, Phoenix or Ecto may change in behaviour and invalidate what’s written here. The sample code to this article is available on GITHUB.

What is Phoenix?

Phoenix is a modern web framework, written in the Elixir programming language, which in turn executes on the Erlang Virtual Machine (BEAM). Phoenix could probably best described as an evolution of Ruby on Rails, taking many of the lessons learned from Rails and improving on them.

Phoenix for Rest APIs

Unlike Rails 5’s upcoming API module, Phoenix applications can be a mixture of pure REST API endpoints, as well as HTML endpoints rendering content dynamically using views, templates, and partials. However, when working with Phoenix, some convenience methods that exist when creating HTML endpoints are missing when working on an API.

One such example is input parameter validation and type coercion, which we will take a closer look at in this article.

Preparation

For the rest of this article we will be working with a small demo API that we are going to set up now using the mix phoenix.new command. If you haven’t installed the Hex Package Manager yet, run the command mix local.hex.

First, we create a new Phoenix application using the mix command. I am using the sqlite database here, the default is postgresql. Either is fine, for our sample app sqlite is sufficient.

$ mix phoenix.new --database sqlite sample_api  
...

Fetch and install dependencies? [Yn] Y
* running mix deps.get
* running npm install && node node_modules/brunch/bin/brunch build

We are all set! Run your Phoenix application:
    $ cd sample_api
    $ mix phoenix.server

You can also run your app inside IEx (Interactive Elixir) as:
    $ iex -S mix phoenix.server

Before moving on, configure your database in config/dev.exs and run:
    $ mix ecto.create

Next, let’s do what Phoenix tell us and create the database, and run:

$ mix ecto.create
...
The database for SampleApi.Repo has been created.

Let’s open up sample_api/web/router.ex and add a new endpoint /hello that will call HelloController.index/2.

defmodule SampleApi.Router do
  use SampleApi.Web, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", SampleApi do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    get "/hello", HelloController, :index
  end
end

Finally we create HelloController under sample_api/web/controllers/hello_controller.ex as follows:

defmodule SampleApi.HelloController do
  use SampleApi.Web, :controller

  def index(conn, %{"name" => name, "age" => age} = params) do
     conn
     |> text("I see, #{name} is #{age} years old!")
  end
  def index(conn, _params) do
    conn
    |> put_status(400)
    |> text("Error, wrong parameters supplied!")
  end
end

HelloController.index/2 has two matches, as our goal is to enforce name and age as required GET parameters for our /hello API endpoint.

Let’s fire up the application using mix phoenix.server and visit the API endpoint, to get the expected results (I’m using HTTPie as my commandline tool):

$ http localhost:4000/hello

HTTP/1.1 400 Bad Request
Error, wrong parameters supplied!
$ http localhost:4001/hello\?name=John\&age=44                                            [3:17:49 pm]

HTTP/1.1 200 OK
I see, John is 44 years old!

Using Ecto Models for Parameter Validation and Type Coercion

The naive implementation shown above gives us a very basic mechanism to ensure that required parameters are present in the GET request. From there on out, we would now have to implement our own logic for checking whether a specified parameter adheres to the specification.

For instance, if we specified in our API contract that age is an integer type with allowed valued between 1 and 120, we might end up adding additional validation code like this:

defmodule SampleApi.HelloController do
  use SampleApi.Web, :controller

  def index(conn, %{"name" => name, "age" => age} = params) do
    if valid_age?(age) do
      conn |> text("I see, #{name} is #{age} years old!")
    else
      conn
      |> put_status(400)
      |> text("Error, age parameter must be and integer between 1 and 120!")
   end
  end
  def index(conn, _params) do
    conn
    |> put_status(400)
    |> text("Error, wrong parameters supplied!")
  end

  def valid_age?(age) do
    if (is_integer(age)) do
      (age >= 1 && age <= 120)
    else
      case (Integer.parse(age)) do
        :error -> false
        {val, _} -> valid_age?(val)
      end
    end
  end
end

This kind of code will quickly become very convoluted and un-maintainable! Luckily, there is a much better way! We can use Ecto Models and the validation logic they offer. For this purpose we will create a new Ecto.Model called HelloParams. However, we won’t be using Ecto to persist HelloParams to any database! The main piece we want from the model is the concept of achangeset!

Let’s create sample_api/web/models/hello_params.ex:

defmodule SampleApi.HelloParams do
  use SampleApi.Web, :model

  schema "HelloParams" do
    field :name, :string
    field :age, :integer
  end

  @required_fields ~w(name age)
  @optional_fields ~w()

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> validate_number(:age, greater_than_or_equal_to: 1, less_than_or_equal_to: 120)
  end
end

Now we can simplify our HelloController logic significantly:

defmodule SampleApi.HelloController do
  use SampleApi.Web, :controller
  alias SampleApi.HelloParams

  def index(conn, params) do
    cs = HelloParams.changeset(%HelloParams{}, params)
    case cs do
      %{:params => %{ "age" => age, "name" => name }, :valid? => true} ->
        conn |> text("I see, #{name} is #{age} years old!")
      _ ->
        conn
        |> put_status(400)
        |> text("Error, wrong parameters supplied!")
    end
  end
end

Notice, how we are matching the changeset cs against %{:valid => true} which is the main logic that tells us whether all of our required parameters were present, and passed the validation and type checks. I’ve used a more detailed match in the example above, to conveniently extract the name and age values into variables that I can immediately use in the String interpolation, but other methods of working with the params is absolutely possible, they are available as cs.params.

Bonus: Error Details

A good API does not only render the proper HTTP status codes, but also gives appropriate error messages and error details. As an extra benefit in using Ecto.Model for our parameter validation and coercion, we can now add error details to our endpoint quite conveniently by parsing the changeset.errors construct:

defmodule SampleApi.HelloController do
  use SampleApi.Web, :controller
  alias SampleApi.HelloParams

  def index(conn, params) do
    cs = HelloParams.changeset(%HelloParams{}, params)
    case cs do
      %{:params => %{ "age" => age, "name" => name }, :valid? => :true} ->
        conn |> text("I see, #{name} is #{age} years old!")
      _ ->
        error_msgs = changeset_errors(cs)
        IO.inspect cs
        conn
        |> put_status(400)
        |> text("Error, wrong parameters supplied!\nDetails: #{error_msgs}")
    end
  end

  def changeset_errors(changeset) do
     changeset.errors
     |> Enum.map(fn {k, v} -> "Parameter #{k} #{render_detail(v)}" end)
  end

  defp render_detail({message, values}) do
    Enum.reduce values, message, fn {k, v}, acc ->
      String.replace(acc, "%{#{k}}", to_string(v))
    end
  end
  defp render_detail(message) do
    message
  end
end

Now calling our endpoint with invalid parameters will render proper error messages:

$ http localhost:4001/hello\?name=John
HTTP/1.1 400 Bad Request

Error, wrong parameters supplied!
Details: Parameter age can't be blank
$ http localhost:4001/hello\?name=John\&age=130
HTTP/1.1 400 Bad Request

Error, wrong parameters supplied!
Details: Parameter age must be less than or equal to 120
$ http localhost:4001/hello\?name=John\&age=two
HTTP/1.1 400 Bad Request

Error, wrong parameters supplied!
Details: Parameter age is invalid

Now that’s pretty amazing, isn’t it?!

Empty Strings vs. Nil

One little issue remains though - consider the following case where we call the API endpoint with the name parameter present, but equal to an empty String:

$ http localhost:4001/hello\?name=\&age=13
HTTP/1.1 200 OK

I see,  is 13 years old!

For HTML endpoints processing form data, Phoenix provides the Phoenix.Controller.scrub_params/2 function that we can plug in a Controller like so:

defmodule SampleApi.SomeController do
  user SampleApi.Web, :controller

  plug :scrub_params, "user" when action in [:new, :create]

  def new(conn, %{"user" => user} = params) do
    # More Code here
  end
end

This assumes that some post "/some/:user" route maps to the SampleApi.SomeController.new/2 function, posting a user form to the endpoint. The scrub_params/2 function will recursively walk through the user struct, replacing all Key-value pairs that look like {k, ""} with {k, nil} (see GitHub for more details). Unfortunately, we can’t use scrub_params/2 with our API endpoint above, where parameters are given as GET parameters.

Scrubber Plug

Let’s fix this by adding our own implementation of Param Scrubbing for ordinary GET params. First, we will create a new file sample_api/lib/sample_api_helpers.ex as follows:

defmodule SampleApi.Helpers do

  def changeset_errors(changeset) do
     changeset.errors
     |> Enum.map(fn {k, v} -> "Parameter #{k} #{render_detail(v)}" end)
  end

  defp render_detail({message, values}) do
    Enum.reduce values, message, fn {k, v}, acc ->
      String.replace(acc, "%{#{k}}", to_string(v))
    end
  end
  defp render_detail(message) do
    message
  end

  def scrub_get_params(conn, _opts) do
     params = conn.params |> Enum.reduce(%{}, &scrub/2 )
     %{conn | params: params}
  end
  defp scrub({k, ""}, acc) do
    Map.put(acc, k, nil)
  end
  defp scrub({k, v}, acc) do
    Map.put(acc, k, v)
  end

end

If you read the scrub_params/2 code on GitHub linked above, you will see that our implementation of scrub_get_params/2 is actually quite similar to the original. Our function scrub_get_params/2 is defined as a function plug, thus accepting a Plug.Conn.t and a Keyword List, and returning a Plug.Conn.t. We walk over all GET params with Enum.reduce/3, using an empty map as the intial accumulator and passing in the private scrub/2 function. That function has two matches, {k, ""} and {k,v}. In the first match, we add {k, nil} to the accumulator, in the second match, we add add {k, v} as it were.

We can now plug scrub_get_params/2 into our SampleApi.HelloController:

defmodule SampleApi.HelloController do
  use SampleApi.Web, :controller
  alias SampleApi.HelloParams
  import SampleApi.Helpers, only: [changeset_errors: 1, scrub_get_params: 2]

  plug :scrub_get_params

  def index(conn, params) do
    cs = HelloParams.changeset(%HelloParams{}, params)
    case cs do
      %{:params => %{ "age" => age, "name" => name }, :valid? => :true} ->
        conn |> text("I see, #{name} is #{age} years old!")
      _ ->
        error_msgs = changeset_errors(cs)
        conn
        |> put_status(400)
        |> text("Error, wrong parameters supplied!\nDetails: #{error_msgs}")
    end
  end
end

Now calling the API endpoint as before yields the proper error:

$ http localhost:4001/hello\?name=\&age=13
HTTP/1.1 400 Bad Request

Error, wrong parameters supplied!
Details: Parameter name can't be blank