How to handle pattern matching exception on def head

I have multiple controller methods, where I am pattern matching with the coming parameters one of them is

  def index(conn, %{
        "id" => camera_exid,
        "from" => from,
        "to" => to,
        "limit" => "3600",
        "page" => _page
      }) do

when user requests such as http://localhost:4000/v2/cameras/pocri-yweod/recordings/snapshots?limit=3600&page=1

it throws an error, and of course, it supposes to through error, but is there any way to handle such error more gracefully than an exception? without creating another index with fewer values to pattern match?

I tried creating a fallback controller very basic

defmodule EvercamMediaWeb.FallbackController do
  use EvercamMediaWeb, :controller

  def call(conn, what) do
    IO.inspect(what)
    render_error(conn, 400, "error.")
  end
end

but it didn’t work.

Is it possible to make it for the whole controller? when the pattern matched parameters doesn’t match, it returns 400 while saying which params are missing? I am the only pattern matching those parameters in the head which are definite.

It won’t work. Optional parameters should not be matched in the head. You should handle them in the body. Name the second parameter and use ‘Map.get’ in the body, with a default value.

A general rule of thumb is that if something does not match the head of a function, it should fail fast. It is not something you expect, so don’t handle it.

1 Like

As a side note, the fallback controller handles the result of your controller.

Why not just make a catch-all index function without pattern-matching and log its parameters? It seems some of your externally arriving data are unexpected.

That’s exactly what I’d recommend you do - a head like:

def index(conn, _bad_params) do
  render_error(conn, 400, "error.")
end

expresses “all four parameters must be supplied or else error” clearly.

HOWEVER

Based on the names of the parameters in your example, I wonder if supplying defaults would be better:

def index(conn, %{"id" => camera_exid} = params) do
  from = Map.get(params, "from", some_kind_of_default)
  # and so on...

  # or, if keeping params together is useful
  params =
    %{"from" => some_default, "to" => some_default, etc}
    |> Map.merge(params)

if I have 20 def’s then having 20 _bad_params? is not an ideal solution.

is there any thing we can do? like a plug or something which can be global solution?

How should that single plug know which parameters would have been required?

It’s easier for each individual action with a catch all clause or just letting it crash with a generic error as it does already.

fallback doesn’t work. can you tell me how to make it work?

I haven’t said “fallback”, you did, and you have been told, it does not what you want, it can only be used to “fix” return values that are not proper conns.

Therefore I am not sure what you are asking for…

I would switch all your def to only pattern match on the absolute required values, which in this case is probably just def index(conn, params). But then inside the body of each function I would call a helper function that will describe the required parameters and pattern match on them, if that match fails, then the helper function can return a 400 error that describes the missing parameters.

2 Likes

I got this solution from StackOverflow. do you second this?

Not particularly.

    required_fields = %{
      index: ["id", "from", "to", "limit", "page"],
      action2: ["id", "x", "y"]
    }

IMO this is a huge smell that this is the wrong place for this logic - it’s in a common path for all actions in the controller, but needs specific behavior for each action.

What about extracting the internals of that to a plug and specifically inserting it for each action?

plug :required_fields, ["id", "from", "to", "etc"] when action == :index
plug :required_fields, ["something", "else"] when action == :action2
1 Like

how I can return a bad request error from this plug?

plug :required_fields, ["id", "from", "to", "etc"] when action == :index

Plugs can call halt on the conn to stop the rest of the process:

        conn
        |> put_status(:bad_request)
        |> text("missing fields: " <> inspect(fields))
        |> halt()
1 Like