Skip to content

Commit f417b72

Browse files
authored
fix: handle wrapper errors with empty inner errors list (#423)
When Ash.Error.Forbidden (or Framework, Invalid, Unknown) is raised with no inner errors, to_json_api_errors flat-mapped over [] and produced zero JSON:API errors. This caused downstream code to proceed as if no error occurred, ultimately crashing with KeyError on missing :result assign. Add a guard (errors != []) to the wrapper-unwrapping clause so empty wrappers fall through to the catch-all, and add ToJsonApiError protocol implementations for the four wrapper types so they produce a proper error response. Closes #422
1 parent d708b78 commit f417b72

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

lib/ash_json_api/error/error.ex

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ defmodule AshJsonApi.Error do
2727
end
2828

2929
def to_json_api_errors(domain, resource, %mod{errors: errors}, type)
30-
when mod in [Forbidden, Framework, Invalid, Unknown] do
30+
when mod in [Forbidden, Framework, Invalid, Unknown] and errors != [] do
3131
Enum.flat_map(errors, &to_json_api_errors(domain, resource, &1, type))
3232
end
3333

@@ -338,6 +338,19 @@ defimpl AshJsonApi.ToJsonApiError, for: Ash.Error.Query.Required do
338338
end
339339
end
340340

341+
defimpl AshJsonApi.ToJsonApiError,
342+
for: [Ash.Error.Forbidden, Ash.Error.Framework, Ash.Error.Invalid, Ash.Error.Unknown] do
343+
def to_json_api_error(error) do
344+
%AshJsonApi.Error{
345+
id: Ash.UUID.generate(),
346+
status_code: AshJsonApi.Error.class_to_status(error.class),
347+
code: to_string(error.class),
348+
title: error.class |> to_string() |> String.capitalize(),
349+
detail: to_string(error.class)
350+
}
351+
end
352+
end
353+
341354
defimpl AshJsonApi.ToJsonApiError, for: Ash.Error.Forbidden.Policy do
342355
def to_json_api_error(error) do
343356
message =

test/acceptance/patch_test.exs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ defmodule Test.Acceptance.PatchTest do
200200
route "/fake_update/:id"
201201
end
202202

203+
patch :forbidden_update do
204+
route "/forbidden_update/:id"
205+
end
206+
203207
related :author, :read
204208
patch_relationship :author
205209
end
@@ -237,6 +241,15 @@ defmodule Test.Acceptance.PatchTest do
237241
end)
238242
end
239243

244+
action :forbidden_update, :struct do
245+
constraints(instance_of: __MODULE__)
246+
argument(:id, :uuid, allow_nil?: false)
247+
248+
run(fn _input, _ ->
249+
{:error, Ash.Error.Forbidden.exception([])}
250+
end)
251+
end
252+
240253
read :by_name do
241254
argument :name, :string do
242255
allow_nil?(false)
@@ -715,6 +728,35 @@ defmodule Test.Acceptance.PatchTest do
715728
end
716729
end
717730

731+
describe "patch with generic action returning forbidden" do
732+
setup do
733+
id = Ecto.UUID.generate()
734+
735+
post =
736+
Post
737+
|> Ash.Changeset.for_create(:create, %{name: "Valid Post", id: id})
738+
|> Ash.create!()
739+
740+
%{post: post}
741+
end
742+
743+
test "returns 403 when generic action returns Forbidden error", %{post: post} do
744+
response =
745+
Domain
746+
|> patch(
747+
"/posts/forbidden_update/#{post.id}",
748+
%{
749+
data: %{attributes: %{}}
750+
},
751+
status: 403
752+
)
753+
754+
assert %{"errors" => [error]} = response.resp_body
755+
assert error["code"] == "forbidden"
756+
assert error["status"] == "403"
757+
end
758+
end
759+
718760
describe "patch with composite primary key" do
719761
setup do
720762
author =

0 commit comments

Comments
 (0)