Skip to content

fix: don’t expose bulk indexes in error source pointers#430

Merged
zachdaniel merged 1 commit intoash-project:mainfrom
gcugnet:bugfix/ensure-consistent-error-source-pointers
Apr 12, 2026
Merged

fix: don’t expose bulk indexes in error source pointers#430
zachdaniel merged 1 commit intoash-project:mainfrom
gcugnet:bugfix/ensure-consistent-error-source-pointers

Conversation

@gcugnet
Copy link
Copy Markdown
Contributor

@gcugnet gcugnet commented Apr 11, 2026

Contributor checklist

Leave anything that you believe does not apply unchecked.

  • I accept the AI Policy, or AI was not used in the creation of this PR.
  • Bug fixes include regression tests
  • Chores
  • Documentation changes
  • Features include unit/acceptance tests
  • Refactoring
  • Update dependencies

Context

After updating all my deps, there were errors in my tests.

Potentially involved Ash updated deps are:

Package Old Version New Version
ash 3.19.1 3.23.1
ash_authentication 4.12.0 4.13.7
ash_json_api 1.6.0-rc.2 1.6.4
ash_postgres 2.6.25 2.8.0

The error was that returned JsonApiError didn’t fitted my helpers anymore to extract the error source (pointer) and assert on received errors, by fields.

The source pointers looked like that in some API responses:

"source" => %{"pointer" => "/data/attributes/0/password"},

rather than expected:

"source" => %{"pointer" => "/data/attributes/password"},

Then I asked AI (Claude Code) to investigate whether it was due to a change in AshJsonApi (I've read the changeset and haven’t found it) or maybe AshAuthentication (the error came from validations provided by this package).

Finally, I think that without AI I wouldn’t have found the error cause easily. So here are AI explanations for this PR, after I brainstormed with it:

AI explanations

NOTE: a bit long but precise on the problem (I've read and validated everything).

Problem

Single-record update operations via JSON:API return error source pointers with an incorrect /0/ bulk index prefix.

Expected:

{
  "errors": [{
    "source": { "pointer": "/data/attributes/password" }
  }]
}

Actual:

{
  "errors": [{
    "source": { "pointer": "/data/attributes/0/password" }
  }]
}

Root Cause

  1. AshJsonApi uses Ash.bulk_update internally for all update operations (since v1.2.0)
  2. Ash v3.21.1+ consistently sets index paths on bulk operation errors via Ash.Actions.Helpers.Bulk.set_index_path/2 -> NOTE: I just updated from Ash v3.19.1 to v3.23.1, so it coincides
  3. AshJsonApi.Error.source_pointer/4 includes the full path (including the integer index) when building source pointers

The index is set on errors during bulk processing to identify which record in a batch caused the error. However, for single-record operations exposed via JSON:API, this index (0) should not appear in the response.

When It Manifests

Affected (includes /0/):

  • Custom validation errors (e.g., validations added via validate in actions)
  • Any errors generated after bulk context is set on the changeset

Not affected (no /0/):

  • Attribute constraint errors (e.g., allow_nil?: falseRequired)
  • Errors generated before bulk context is set on the changeset

Proposed Fix

Filter out integer indices from the path in source_pointer/4:

defp source_pointer(resource, field, path, :action) do
  # Filter out integer indices from bulk operations (e.g., 0 from bulk_update)
  # Single-record JSON:API operations should not include array indices in source pointers
  clean_path = path |> List.wrap() |> Enum.reject(&is_integer/1)
  json_key = AshJsonApi.Resource.Info.field_to_json_key(resource, field)
  "/data/attributes/#{Enum.join(clean_path ++ [json_key], "/")}"
end

defp source_pointer(resource, field, path, type)
     when type in [:create, :update] and not is_nil(field) do
    # Filter out integer indices from bulk operations (e.g., 0 from bulk_update)
    # Single-record JSON:API operations should not include array indices in source pointers
  clean_path = path |> List.wrap() |> Enum.reject(&is_integer/1)

  if clean_path == [] && Ash.Resource.Info.public_relationship(resource, field) do
    "/data/relationships/#{field}"
  else
    json_key = AshJsonApi.Resource.Info.field_to_json_key(resource, field)
    "/data/attributes/#{Enum.join(clean_path ++ [json_key], "/")}"
  end
end

This strips integer indices (like 0) from the path while preserving any string-based nested path segments.

Considerations

  • Bulk endpoints: If AshJsonApi adds first-class bulk operation endpoints in the future, those should include the index in error responses to identify which record failed. This fix specifically targets single-record operations. The implementation may need to be revisited when bulk endpoints are added.
  • Backwards compatibility: This is a bug fix that aligns the behavior with JSON:API spec. Applications relying on the incorrect /0/ prefix would need to update, but such reliance would itself be a bug.

My added notes

  • A new test have been added with my PR: it passes with the fix, and fails if you remove the fix (showing the problem)
  • I tried the fix locally in my project by changing the :ash_json_api path to my local fork, and it solves the errors I encountered after deps update
  • 4 tests were failing before my commit, they are still after (I haven’t fixed tests non-related to my PR) -> same tests failing in you CI: https://github.com/ash-project/ash_json_api/actions/runs/23875212681/job/69617090510
    • These failing tests seems easy to fix, (some types are :integer in the result and :string in the expected block, but I haven’t dig on which type is the right one)

Comment thread lib/ash_json_api/error/error.ex Outdated
end

defp source_pointer(resource, field, path, :action) do
clean_path = path |> List.wrap() |> Enum.reject(&is_integer/1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may have errors pointing at nested paths for list attributes or relationships etc. Instead what we should do is modify the callsite that uses Ash.bulk_update to handle the error response and remove the first item in it if its 0 exactly IMHO.

Copy link
Copy Markdown
Contributor Author

@gcugnet gcugnet Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, it feels cleaner to handle it just after the response from Ash.bulk_update/3. I edited my commit with a new proposal.

The new test still passes, and all the tests from my project also pass with the :ash_json_api deps pointing to the local fork.

Also, I think I should modify the :update for :destroy as the operation argument passed to add_error/4 line 638 of AshJsonApi.Controllers.Helpers module (that I already modified). Do you agree?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, agreed 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just updated my commit to include this little fix. Do you mind the comment before the private function strip_bulk_index_from_errors/1 or is it good for you?

@gcugnet gcugnet force-pushed the bugfix/ensure-consistent-error-source-pointers branch from 67d5393 to 107a562 Compare April 11, 2026 17:34
@gcugnet gcugnet force-pushed the bugfix/ensure-consistent-error-source-pointers branch from 107a562 to d17267c Compare April 12, 2026 19:51
@zachdaniel zachdaniel merged commit 6e5fa4f into ash-project:main Apr 12, 2026
20 of 21 checks passed
@zachdaniel
Copy link
Copy Markdown
Contributor

🚀 Thank you for your contribution! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants