Best practices for validation of UUIDs in GET requests

Here is the setting:

  • Say we have a ressource User which has a Ecto.UUID as primary key.
  @primary_key {:id, Ecto.UUID, autogenerate: true}
  • When GETting the resource a UUID must be provided (GET /users/c8588b10-5c0a-4f74-9f43-f2df7ddc7342). But if an invalid parameter instead of a UUID is sent, postgres is raising the exception:
** (exit) an exception was raised:
    ** (Ecto.Query.CastError) deps/ecto/lib/ecto/repo/queryable.ex:322: value `"1"` in `where` cannot be cast to type Ecto.UUID in query:

So now we have two choices:

  1. Catch the Error with Plug.Exception and raise a 404
  2. Validate that the given parameter is a valid UUID before accessing the database

The first approach doesn’t feel right, because we have to hope, that the Ecto.Query.CastError is only ever raised when there is a wrong UUID-format given. Every other CastError would lead to the same 404 without knowing the difference.

So for the second approach: What is the best way to do that? Create a specific changeset which checks the format of the UUID and rejects the Request with a 422 or 404 on failure? Or is there a better (built-in) way?

Just an idea, but you can use Plug that’d be executed before the requests are being handled in controller that validates the format of IDs. You would write and then configure the plug on a controller level this way:

plug RequireUUIDs, param_keys: [:id, :organization_id]

as an example. So if you have nested routes it’d validate :id and :organization_id, and if these are present, and are not UUIDs, it would raise 404.

Thanks! That sounds promising, I will try that.

Sorry for resurrecting such an old thread, but I wanted to fix this in my LiveViews and thought my solution might help others (or they may have a better take.)

I added an on_mount helper to my LiveView in my_app.ex:

  def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {MyApp.Layouts, :app}

      on_mount MyAppWeb.LiveValidateIds

      unquote(html_helpers())
    end
  end

And here’s the helper:

defmodule MyAppWeb.LiveValidateIds do
  @moduledoc """
  Helper to ensure known id params are UUIDs.
  """

  require Logger

  defmodule InvalidUUIDError do
    @moduledoc false
    defexception message: "unable to cast ids to UUID", plug_status: 404
  end

  def on_mount(:default, params, _session, socket) do
    validate_params!(params)

    {:cont, socket}
  end

  defp validate_params!(params) do
    for {key, value} <- params do
      if String.ends_with?(key, "_id") || key == "id" do
        validate_param!(key, value)
      end
    end
  end

  defp validate_param!(key, value) do
    case Ecto.UUID.cast(value) do
      {:ok, _} ->
        :ok

      :error ->
        Logger.warning("Invalid UUID for param #{key}: #{value}")
        raise InvalidUUIDError
    end
  end
end