AshJsonApi custom rest api error

Hi

How to add generate custom JSON error for a JSON API. I see that we can do Handle Errors — ash v2.17.22 but how to generate JSON error say 409 from resource action blocks. Any pointers would be great.

Thanks
Srikanth

There wasn’t a way to do this, but we have had this pattern in other places for a while, so I just released a version of ash_json_api that supports this.

You would make a custom error, and implement AshJsonApi.ToJsonApiError for it.

For example:

  defmodule NotAvailable do
    use Ash.Error.Exception

    def_ash_error([:reason], class: :invalid)

    defimpl AshJsonApi.ToJsonApiError do
      def to_json_api_error(error) do
        %AshJsonApi.Error{
          id: Ash.ErrorKind.id(error),
          status_code: 409,
          code: Ash.ErrorKind.code(error),
          title: Ash.ErrorKind.code(error),
          detail: Ash.ErrorKind.message(error)
        }
      end
    end

    defimpl Ash.ErrorKind do
      def id(_), do: Ash.UUID.generate()
      def code(_), do: "not_available"
      def message(error), do: "Not available: \#{error.reason}"
    end
  end

Then you’d use that exception:

change fn changeset, _ -> 
  ...
  Ash.Changeset.add_error(changeset, NotAvailable.exception([])
end
1 Like

That is super quick Zach. Features are rolling out like chocolate from Chocolate Factory.
You indeed Willy Wonka.

Hi @zachdaniel

How to have a custom json api error in a create action. I tried your code seems for create
I have done

create action is like this

    create :create do                                                                                                                                                             
      primary?(true)                                                                                                                                                              
                                                                                                                                                                                  
      error_handler {__MODULE__, :handle_errors, []}                                                                                                                              
    end
def handle_errors(changeset, error) do 
    case error do                                                                                                                                                                 
      %Ash.Error.Changes.InvalidAttribute{} ->                                                                                                                                    
        if error.message == "violates an exclusion constraint" do   
           Ash.Changeset.add_error(changeset, NotAvailable.exception([])
        else
           changeset
        end
   end
end

Interesting. That should work I think? Have you verified that your function is getting called and is getting to your add_error call?

Hi @zachdaniel

I have checked it is hitting the handle_errors function. Here are the stacktraces and logs

06:52:52.691 request_id=F6jjS5DwNnVpVNcAABGj [info] absence_setting.ex exclusion constraint                                                                                       
%AshJsonApi.Error.Conflict{                                                                                                                                                       
  reason: nil,                                                                                                                                                                    
  changeset: nil,                                                                                                                                                                 
  query: nil,                                                                                                                                                                     
  error_context: [],                                                                                                                                                              
  vars: [],                                                                                                                                                                       
  path: [],                                                                                                                                                                       
  stacktrace: #Stacktrace<>,                                                                                                                                                      
  class: :invalid                                                                                                                                                                 
}                                                                                                                                                                                 
%Ash.Error.Changes.InvalidChanges{                                                                                                                                                
  fields: [],                                                                                                                                                                     
  message: nil,                                                                                                                                                                   
  validation: nil,                                                                                                                                                                
  value: nil,                                                                                                                                                                     
  changeset: nil,                                                                                                                                                                 
  query: nil,                                                                                                                                                                     
  error_context: [],                                                                                                                                                              
  vars: [],                                                                                                                                                                       
  path: [],                                                                                                                                                                       
  stacktrace: #Stacktrace<>,                                                                                                                                                      
  class: :invalid                                                                                                                                                                 
}                              

warning: the following fields are unknown when raising Ash.Error.Unknown.UnknownError: [value: []]. Please make sure to only give known fields when raising or redefine Ash.Error$
  (ash 2.17.22) lib/ash/error/unknown/unknown_error.ex:5: Ash.Error.Unknown.UnknownError."exception (overridable 1)"/1                                                            
  (ash 2.17.22) lib/ash/error/exception.ex:58: Ash.Error.Unknown.UnknownError.exception/1                                                                                         
  (ash 2.17.22) lib/ash/error/error.ex:441: Ash.Error.to_ash_error/3                                                                                                              
  (ash 2.17.22) lib/ash/error/error.ex:227: Ash.Error.to_error_class/2                                                                                                            
  (ash 2.17.22) lib/ash/actions/create/create.ex:132: Ash.Actions.Create.do_run/4                                                                                                 
  (ash 2.17.22) lib/ash/actions/create/create.ex:45: Ash.Actions.Create.run/4                                                                                                     
  (reservation 0.1.0) lib/reservation/reserve/api.ex:1: Reservation.Reserve.Api.create/2                                                                                          
  (ash_json_api 0.34.2) lib/ash_json_api/controllers/helpers.ex:94: anonymous fn/1 in AshJsonApi.Controllers.Helpers.create_record/1                                              
  (ash_json_api 0.34.2) lib/ash_json_api/controllers/post.ex:19: AshJsonApi.Controllers.Post.call/2                                                                               
  (reservation 0.1.0) deps/plug/lib/plug/router.ex:246: anonymous fn/4 in ReservationWeb.Plugs.ReservationRouter.dispatch/2                                                       
  (telemetry 1.2.1) /home/prasy/Projects/elixir/reservation/deps/telemetry/src/telemetry.erl:321: :telemetry.span/3                                                               
  (reservation 0.1.0) deps/plug/lib/plug/router.ex:242: ReservationWeb.Plugs.ReservationRouter.dispatch/2                                                                         
  (reservation 0.1.0) lib/reservation_web/plugs/reservations_router.ex:1: ReservationWeb.Plugs.ReservationRouter.plug_builder_call/2                                              
  (phoenix 1.7.10) lib/phoenix/router/route.ex:42: Phoenix.Router.Route.call/2                                                                                                    
  (phoenix 1.7.10) lib/phoenix/router.ex:432: Phoenix.Router.__call__/5                                                                                                           
  (reservation 0.1.0) lib/reservation_web/endpoint.ex:1: ReservationWeb.Endpoint.plug_builder_call/2                                                                              
  (reservation 0.1.0) lib/reservation_web/endpoint.ex:1: ReservationWeb.Endpoint.call/2                                                                                           
  (phoenix 1.7.10) lib/phoenix/test/conn_test.ex:225: Phoenix.ConnTest.dispatch/5                                                                                                 
                                                                                                                                                                                  
06:52:52.730 request_id=F6jjS5DwNnVpVNcAABGj [error] FrameworkError: Framework Error | something went wrong. %Ash.Error.Unknown.UnknownError{error: nil, field: nil, changeset: n$
.06:52:52.730 request_id=F6jjS5DwNnVpVNcAABGj [info] Sent 500 in 77ms 

I am using

ash: 2.17.22
ash_json_api: 0.34.2
ash_postgres: 1.3.68

Is the rest of this line available in the logs?
06:52:52.730 request_id=F6jjS5DwNnVpVNcAABGj [error] FrameworkError: Framework Error | something went wrong. %Ash.Error.Unknown.UnknownError{error: nil, field: nil, changeset: n$

That would help me figure out where the issue is.

Hi

I am checking it. I don’t have any further logs. I am adding logs. I will post it.

Hi

If I write the handle_error like the following then its works.
What I did not understand is the handle_errors function is being invoked twice.
Not sure why.

   def handle_errors(changeset, error) do                                                                                                                                                                                                                                                                                     
-    # https://hexdocs.pm/ash/Ash.Changeset.html#handle_errors/2                                                                                                                                                                                                                                                              
     case error do                                                                                                                                                                                                                                                                                                            
       %Ash.Error.Changes.InvalidAttribute{} ->                                                                                                                                                                                                                                                                               
         if error.message == "violates an exclusion constraint" do                                                                                                                                                                                                                                                            
           Logger.info("absence_setting.ex exclusion constraint")                                                                                                                                                                                                                                                             
          {changeset, AshJsonApi.Error.Conflict.exception([])}                                                                                                                                                                                                                                                               
        else                                                                                                                                                                                                                                                                                                                 
          :ignore                                                                                                                                                                                                                                                                                                            
         end                                                                                                                                                                                                                                                                                                                  
      some_error ->                                                                                                                                                                                                                                                                                                          
        some_error                                                                                                                                                                                                                                                                                                           
     end                                                                                                                                                                                                                                                                                                                      
   end 

It should be invoked for each error on the changeset. Is it called with a different error each time?

First error is

%Ash.Error.Changes.InvalidAttribute{                                                                                                                                                                                                                                                                                          
  field: :start_date,                                                                                                                                                                                                                                                                                                         
  message: "violates an exclusion constraint",                                                                                                                                                                                                                                                                                
  private_vars: [constraint: "overlapping_absence", constraint_type: :exclusion],                                                                                                                                                                                                                                             
  value: nil,                                                                                                                                                                                                                                                                                                                 
  changeset: nil,                                                                                                                                                                                                                                                                                                             
  query: nil,                                                                                                                                                                                                                                                                                                                 
  error_context: [],                                                                                                                                                                                                                                                                                                          
  vars: [],                                                                                                                                                                                                                                                                                                                   
  path: [],                                                                                                                                                                                                                                                                                                                   
  stacktrace: #Stacktrace<>,                                                                                                                                                                                                                                                                                                  
  class: :invalid                                                                                                                                                                                                                                                                                                             
}  

The second time, the error returned i.e Conflict error this is looped back

%AshJsonApi.Error.Conflict{                                                                                                                                                                                                                                                                                                   
  reason: nil,                                                                                                                                                                                                                                                                                                                
  changeset: nil,                                                                                                                                                                                                                                                                                                             
  query: nil,                                                                                                                                                                                                                                                                                                                 
  error_context: [],                                                                                                                                                                                                                                                                                                          
  vars: [],                                                                                                                                                                                                                                                                                                                   
  path: [],                                                                                                                                                                                                                                                                                                                   
  stacktrace: #Stacktrace<>,                                                                                                                                                                                                                                                                                                  
  class: :invalid                                                                                                                                                                                                                                                                                                             
}    

But if I write the handle_error like this

  def handle_errors(_changeset, error) do                                                                                                                                                                                                                                                                                     
    # https://hexdocs.pm/ash/Ash.Changeset.html#handle_errors/2                                                                                                                                                                                                                                                               
    case error do                                                                                                                                                                                                                                                                                                             
      %Ash.Error.Changes.InvalidAttribute{} ->                                                                                                                                                                                                                                                                                
        if error.message == "violates an exclusion constraint" do                                                                                                                                                                                                                                                             
          AshJsonApi.Error.Conflict.exception([])                                                                                                                                                                                                                                                                             
        else                                                                                                                                                                                                                                                                                                                  
          :ignore                                                                                                                                                                                                                                                                                                             
        end                                                                                                                                                                                                                                                                                                                   
                                                                                                                                                                                                                                                                                                                              
      some_error ->                                                                                                                                                                                                                                                                                                           
        some_error                                                                                                                                                                                                                                                                                                            
    end                                                                                                                                                                                                                                                                                                                       
  end    

Error is not looped back.

Interesting…I’ll need to look into it. It looks like you’ve got a workaround for now though.

1 Like

Yeah it works for now. I can continue with my ash project.