How to use Absinthe.MiddleWare to catch exception?

Hello, everyone, I have some code like this:

  def update_package(_, %{input: params}, _) do
    p = Repo.get(Core.Package, params.id)

    case p do
      %Core.Package{} = p ->
        with {:ok, package} <- Core.update_package(p, params) do
          {:ok, %{package: package}}
        end

      _ ->
        {:error, "Package by id #{params.id} do not exist."}
    end
  end

  def delete_package(_, %{input: params}, _) do
    p = Repo.get(Core.Package, params.id)

    case p do
      %Core.Package{} = p ->
        with {:ok, package} <- Core.delete_package(p) do
          {:ok, %{package: package}}
        end

      _ ->
        {:error, "Package by id #{params.id} do not exist."}
    end
  end

I have some entity: package, card and others, every time when I write a mutation resolver function, I have to write

      _ ->
        {:error, "Package by id #{params.id} do not exist."}
    end

I hate this duplicated.

How do I use Absinthe.Middleware to do the same things like phoenix’s action_fallback to catch the not_found error

I have read the craft-graphql book, the middleware example shows how to convert errors, I got that, but I don’t know to use it to catch exception.

I expect I can use a middleware to wrap the mutation, then I can write code like:

  def update_package(_, %{input: params}, _) do
    p = Repo.get!(Core.Package, params.id)  # use get! to raise a exception

    with {:ok, package} <- Core.update_package(p, params) do
      {:ok, %{package: package}}
    end
  end

Do you have some idea? Thanks!

To be clear, Absinthe has been doing what action_fallback does since long before action_fallback existed. action_fallback isn’t about exceptions it’s about non %Plug.Conn{} return values, which is exactly how the book guides you towards using Absinthe middleware.

In my own projects I basically always add to my MyApp.Repo module these functions:

  def fetch(query, id) do
    query
    |> Ecto.Query.where(id: ^id)
    |> fetch
  end

  def fetch(query) do
    case all(query) do
      [] -> {:error, query}
      [obj] -> {:ok, obj}
      _ -> raise "Expected one or no items, got many items #{inspect(query)}"
    end
  end

This would let you do:

def update_package(_, %{input: params}, _) do
  with {:ok, package} <- Repo.fetch(Core.Package, params.id),
  {:ok, package} <- Core.update_package(p, params) do
    {:ok, %{package: package}}
  end
end

Now, if the package isn’t found it’ll return {:error, %EctoQuery{}} so I then define middleware that runs after resolvers to convert that shape into a nice error message:

%{from: %{source: {_, queryable}}} = query
schema = queryable |> Module.split() |> List.last()
{:error, "#{schema} not found"}

This to me is a much cleaner approach than exceptions. And unlike action_fallback we didn’t need to create a different mechanism to handle it, since Absinthe middleware doesn’t halt the way plug does.

Catching exceptions is still possible by replacing the Absinthe.Resolution middleware with your own middleware that calls the resolver function inside of a try block.

8 Likes

Thank you so much, ben, That’s amazing code, very clear.

1 Like

I stumbled on this searching for the same info OP was. After reading @benwilson512’s recommendation I was able to use middleware to achieve something pretty similar.

# schema.ex
    @desc "Update a user"
    field :update_user, type: :user do
      arg(:id, non_null(:id))
      arg(:attrs, :user_params)

      middleware(Middleware.Resource, Accounts.User)

      resolve(fn _parent, %{attrs: attrs}, %{context: %{resource: user}} ->
        # update handler
      end)
    end

# Middleware.Resource
defmodule MyApp.Middleware.Resource do
  @behaviour Absinthe.Middleware

  def call(%{arguments: %{id: id}} = resolution, schema) do
    case MyApp.Repo.get(schema, id) do
      nil ->
        resolution
        |> Absinthe.Resolution.put_result({:error, "not found"})

      resource ->
        %{resolution | context: Map.put(resolution.context, :resource, resource)}
    end
  end
end

Something I just came up with so I don’t know if it will work out, but it makes sense to me so I thought I’d share while I’m here.

Hi, @tfwright, I think it will works, but that’s not clear by putting the resource to context and fetch them again.

Yes, there’s certainly a trade-off here between clarity and DRYness. When using a RESTful controller architecture I actually prefer to have this logic repeated in every action because I like my controller logic to be as simple as possible. But for some reason it feels simpler to me to reuse code for that here. :man_shrugging:

There may be other technical reasons not to use context in this way, I’m not sure.