Hi,
I have an issue with a GraphQL api developed with Absinthe.
Today, for errors returned from my context functions that I consider GraphQL errors (not supposed to happen if the api is used correctly), here is what I do:
defmodule MyAppWeb.Resolver.Accounts.ChangeUserUsername do
@moduledoc false
alias MyAppWeb.Resolver.Accounts.Error
alias MyAppWeb.Resolver.Success
def resolve(_parent, %{input: input}, %{context: %{principal: principal}} = resolution) do
raw_input = Map.get(resolution.definition.argument_data, :input)
case MyApp.Accounts.change_user_username(input, principal: principal) do
{:ok, _user} ->
{:ok, %Success{}}
{:error, :user_not_found} ->
%{user_id: user_id} = raw_input
{:ok,
%Error.UserNotFound{
message: "No user was found with ID \"#{user_id}\"."
}}
{:error, :username_already_taken} ->
%{username: username} = raw_input
{:ok,
%Error.UsernameAlreadyTaken{
message: "Username \"#{username}\" is already taken by another user."
}}
{:error, error} ->
{:middleware, MyAppWeb.Middleware.HandleError, error}
end
end
end
Ultimately, I return {:middleware, MyAppWeb.Middleware.HandleError, error}
to process errors that are not supposed to happen. All my resolvers work the same way.
Here is the middleware source:
defmodule MyAppWeb.Middleware.HandleError do
@moduledoc false
@behaviour Absinthe.Middleware
require Logger
alias MyAppWeb.Resolver.GraphQLErrors
@impl true
def call(resolution, error) do
graphql_error = handle_error(error, resolution)
Absinthe.Resolution.put_result(resolution, {:error, graphql_error})
end
defp handle_error({:validation_failure, changeset}, resolution) do
GraphQLErrors.invalid_input_argument(changeset, fn field ->
resolution.adapter.to_external_name(field, :field)
end)
end
defp handle_error({:unauthorized, reason}, _resolution) do
GraphQLErrors.access_forbidden(reason)
end
defp handle_error(error, _resolution) do
Logger.error(fn ->
"Unexpected error when resolving query: #{inspect(error)}"
end)
GraphQLErrors.unexpected_error()
end
end
defmodule MyAppWeb.Resolver.GraphQLErrors do
@moduledoc false
alias MyAppWeb.Resolver.ErrorFormatter
def unauthenticated do
%{
message: "Cannot process the query because no principal is authenticated.",
extensions: %{
code: "UNAUTHENTICATED"
}
}
end
def access_forbidden(reason) do
%{
message: "Access forbidden.",
extensions: %{
code: "FORBIDDEN",
details: reason
}
}
end
def invalid_input_argument(changeset, convert_field_name) do
%{
message: "Argument \"input\" has invalid value.",
extensions: %{
code: "INVALID_ARGUMENT",
details: ErrorFormatter.format(changeset, convert_field_name)
}
}
end
def unexpected_error do
%{
message: "An unexpected error happened.",
extensions: %{
code: "UNEXPECTED_ERROR"
}
}
end
end
The job of the middleware is to turn cryptic error keywords and details into friendly GraphQL errors with a proper message and error code.
My issue is that I have to explicitly return the middleware from all my resolver functions. Is there a way (middleware, plugin or else) to add additional processing after the resolver function returns but before the call to put_result/2
?
I cannot just place a middleware after the resolve function and process the errors from the resolution struct because there might be other errors unrelated to the result of the resolver function.
I would like to run something like:
def call(resolution, result) do
with {:error, error} <- result do
graphql_error = handle_error(error, resolution)
{:error, graphql_error}
end
end
That would simplify all my resolver functions. Is this possible? recommended?