How to stop a field resolution after a middleware returns an error?

In my GraphQL schema, I am using an authentication middleware similar to the one in the documentation. This middleware is set for all mutation fields.
In some mutation fields, I also want to use the Absinthe.Relay.Node.ParseIDs middleware.

In case a user is not authenticated and submits a mutation with an input that does not pass the ParseIDs middleware, the server returns the error message:

In argument \"input\": In field \"userId\": Could not decode ID value `foobar'

but I was expecting:

unauthenticated

After reading the Absinthe.Middleware documentation, I understand that both the authentication and ParseIDs middlewares are run anyway.

All middleware on a field are always run, make sure to pattern match on the state if you care.

When no user is authenticated, is there a way to receive the error message “unauthenticated” regardless of the input provided?

Are you not getting the unauthorized error at all? Or are you getting both unauthorized AND the parseids error?

What absinthe / absinthe_relay error are you getting?

I only get the ParseIDs error message if no user is authenticated and the userId is invalid. If no user is authenticated and the userId is valid, then I get the error message from the authentication middleware.

$ mix test test/myapp_web/schema_accounts_test.exs:163
Excluding tags: [:test]
Including tags: [line: "163"]



  1) test given no user is signed in, changeUserUsername should return an error with code 'UNAUTHENTICATED' (MyappWeb.SchemaAccountsTest)
     test/myapp_web/schema_accounts_test.exs:163
     match (=) failed
     code:  assert %{"data" => %{"changeUserUsername" => nil}, "errors" => [%{"message" => "Cannot process the query because no principal is authenticated.", "path" => ["changeUserUsername"], "extensions" => %{"code" => "UNAUTHENTICATED"}}]} = result
     right: %{
              "data" => %{"changeUserUsername" => nil},
              "errors" => [
                %{
                  "locations" => [%{"column" => 0, "line" => 2}],
                  "message" => "In argument \"input\": In field \"userId\": Could not decode ID value `foobar'",
                  "path" => ["changeUserUsername"]
                }
              ]
            }
     stacktrace:
       test/myapp_web/schema_accounts_test.exs:177: (test)



Finished in 1.2 seconds
43 tests, 1 failure, 42 excluded

Randomized with seed 365933

How are you setting up the middleware?

The authentication middleware is setup in the schema:

defmodule MyappWeb.Schema do
  @moduledoc false

  use Absinthe.Schema

  use Absinthe.Relay.Schema,
    flavor: :modern

  import_types(MyappWeb.Schema.Types.Common)
  import_types(MyappWeb.Schema.Types.Accounts)
  import_types(MyappWeb.Schema.Types.Authentication)

  import_types(MyappWeb.Schema.Common)
  import_types(MyappWeb.Schema.Accounts)
  import_types(MyappWeb.Schema.Authentication)

  query do
    import_fields(:common_queries)
    import_fields(:authentication_queries)
  end

  mutation do
    import_fields(:accounts_commands)
    import_fields(:authentication_commands)
  end

  def middleware(
        middleware,
        %Absinthe.Type.Field{identifier: field_identifier},
        %Absinthe.Type.Object{identifier: object_identifier}
      )
      when field_identifier not in [:sign_in_user, :sign_out_user] and
             object_identifier in [:query, :subscription, :mutation] do
    [MyappWeb.Middleware.EnsureAuthenticated | middleware]
  end

  def middleware(middleware, _field, _object) do
    middleware
  end
end

Here is my authentication middleware:

defmodule MyappWeb.Middleware.EnsureAuthenticated do
  @behaviour Absinthe.Middleware

  def call(resolution, _opts) do
    case resolution.context do
      %{principal: _principal} ->
        resolution

      _ ->
        resolution
        |> Absinthe.Resolution.put_result({:error, unauthenticated()})
    end
  end

  defp unauthenticated() do
    %{
      message: "Cannot process the query because no principal is authenticated.",
      extensions: %{
        code: "UNAUTHENTICATED"
      }
    }
  end
end

And the ParseIDs middleware is setup this way:

defmodule MyappWeb.Schema.Accounts do
  @moduledoc false

  use Absinthe.Schema.Notation
  use Absinthe.Relay.Schema.Notation, :modern

  alias MyappWeb.Resolvers.Accounts

  object :accounts_commands do
    @desc "Change the username of a user."
    field(:change_user_username, type: :change_user_username_payload) do
      arg(:input, non_null(:change_user_username_input))

      middleware(Absinthe.Relay.Node.ParseIDs, input: [user_id: :user])
      resolve(&Accounts.ChangeUserUsername.resolve/3)
    end
  end
end

For information, there is no problem when I also use Absinthe.Relay.Mutation.Notation.Modern and setup the ParseIDs middleware this way:

defmodule MyappWeb.Schema.Accounts do
  @moduledoc false

  use Absinthe.Schema.Notation
  use Absinthe.Relay.Schema.Notation, :modern

  alias MyappWeb.Resolvers.Accounts

  object :accounts_commands do
    @desc "Change the username of a user."
    payload field(:change_user_username) do
      @desc "Input type of ChangeUserUsername."
      input do
        @desc "The ID of the user."
        field(:user_id, non_null(:id))
        @desc "The username of the user."
        field(:username, non_null(:string))
      end

      @desc "Return type of ChangeUserUsername."
      output do
        @desc "Indicates if the command succeeded."
        field(:success, non_null(:boolean))
        @desc "Error details."
        field(:error, :error)
      end

      middleware(Absinthe.Relay.Node.ParseIDs, user_id: :user)
      resolve(&Accounts.ChangeUserUsername.resolve/3)
    end
  end
end

@itopiz

        resolution
        |> Absinthe.Resolution.put_result({:error, unauthenticated()})

changes the state of the Resolution struct to resolved.
So you can add a clause in ParseIDs middleware to catch this case and pass like this:

  def call(%Resolution{state: :resolved} = resolution, _) do
    resolution
  end
1 Like

Thank you for the suggestion. However, the ParseIDs middleware being part of the absinthe_relay project, I cannot change it.

The ParseIDs middleware should definitely be pattern matching on the unresolved state, I’ll look at fixing that today.