rmosolgo / rmosolgo/graphql-ruby
Returning ALL Invalid ID Errors for `loads:` Arguments in JSON Response
Nobody has claimed this yet.
- Dominant language
- Ruby
- Stars
- 5.4k
- Forks
- 1.4k
- Avg merge
- 23h 19m
- Merged PRs (30d)
- 28
Description
Return ALL Invalid ID Errors for loads: Arguments in JSON Response
DISCLAIMER: This is more of a question of "how to patch this" more than a feature request, since from what I've seen and read it's not straightforward to achieve neither a priority.
Use Case
When GraphQL queries passes multiple invalid IDs, on an array arguments that uses autoloaded, we want to return errors for ALL invalid IDs in a single response, rather than just the first one. This would ease some scenarios where our frontend has to parse the error message to identify which ID was invalid, and then re-submit the query without that ID, so the error is gracefully handled for the user. This can lead to multiple round-trips if there are several invalid IDs.
(Example: they accessed a bookmarked URL to our programs marketplace, where a filter by label is applied, but some of those labels have been deleted since).
Example Query object:
class Queries::Programs < BaseQuery
description 'Fetch a list of programs by IDs'
type [Types::Programs::ProgramType], null: false
argument :program_ids, [ID], required: true, loads: Types::Programs::ProgramType
def resolve(programs:)
programs
end
end
Example request with multiple invalid IDs:
query {
programs(programIds: ["valid-id", "invalid-1", "invalid-2", "invalid-3"]) {
id
name
}
}
Default response (only returns first invalid id error):
{
"errors": [
{
"message": "No object found for `programIds: \"invalid\"`",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"programs"
]
}
],
"data": null
}
Example of desired response (all invalid IDS returned, with a structured format):
{
"errors": [
{
"message": "Record not found: invalid-1",
"extensions": { "code": "INVALID_ID", "program_id": "invalid-1" }
},
{
"message": "Record not found: invalid-2",
"extensions": { "code": "INVALID_ID", "program_id": "invalid-2" }
}
],
"data": null
}
Attempted Solution
On our app, we tried overriding load_application_object_failed to collect errors instead of raising immediately, on all BaseMutation, BaseQuery, and InputObject levels:
def load_application_object_failed(error)
puts "1) Inside load_application_object_failed"
ctx[:failed_loads] ||= []
ctx[:failed_loads] << {
path: error.path,
argument: error.argument.graphql_name,
invalid_id: error.id
}
nil # Return nil instead of raising
end
Then, on BaseField, we tried overriding the resolve so that it first checks for any collected errors and raises them all at once before proceeding with the resolver:
def resolve(obj, args, ctx)
puts "2) Inside BaseField resolve. load errors in context: #{ctx[:failed_loads].size}"
failed_loads = ctx[:failed_loads]
return if failed_loads.blank?
failed_loads.each_with_index do |failed_load, index|
error = GraphQL::ExecutionError.new("Record not found: #{failed_load[:invalid_id]}")
error.path = failed_load[:path]
error.extensions = {
'code' => 'INVALID_ID',
'argument' => failed_load[:argument],
'invalid_id' => failed_load[:invalid_id]
}
is_last_error = index == failed_loads.size - 1
raise error if is_last_error # Halt execution on last error to avoid running resolver
ctx.add_error(error)
end
super
end
The Problem: Inconsistent Timing Behavior
Our solution works for Input Objects but fails for Direct Arguments due to different execution timing:
✅ Works: Input Objects with loads:
class MyInputObject < GraphQL::Schema::InputObject
argument :program_ids, [ID], loads: ProgramType
end
class MyQuery < GraphQL::Schema::Resolver
argument :filters, MyInputObject
def resolve(filters:)
raise "Should not reach here if there are invalid IDs"
end
end
Execution Order:
load_application_object_failedcalled (prepare phase)- BaseField
resolveruns with collected errors on context, so adds them to context and raises the last one ✅ - Query resolver never runs ✅
❌ Fails: Direct Arguments with loads:
class MyQuery < GraphQL::Schema::Resolver
argument :program_ids, [ID], loads: ProgramType
def resolve(filters:)
raise "Should not reach here if there are invalid IDs" # ❌ This runs anyway
end
end
Execution Order:
- BaseField
resolveruns, but no errors yet in context so does nothing load_application_object_failedcalled (from resolve phase)- Query resolver runs ❌
Root Cause Analysis
The issue stems from when loads: processing occurs:
- Input Objects:
loads:processed during prepare phase (early) - Direct Arguments:
loads:processed during resolve phase (late)
This timing difference makes me unsure of how to create a global solution that works for both cases.
This appears related to https://github.com/rmosolgo/graphql-ruby/issues/2966 which discusses similar challenges with loads: error handling timing.
Questions
Recommended patterns? What's the recommended way to handle batch invalid ID scenarios in GraphQL-Ruby? Do you personally recommend not using loads: on array arguments at all? Or is there a better way to patch this globally on my application that I'm missing?
Thank you so much in advance!
Environment
- Ruby: 3.1.0
- GraphQL-Ruby: 2.0.x
- Rails: 7.2.x
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing load_application_object_failed and BaseField#resolve for direct arguments, then compare that execution path with GraphQL::Schema::InputObject and the related issue #2966. Done means the behavior for multiple invalid IDs is clearly established for both argument forms, with a documented recommended pattern or a focused change and regression coverage.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- graphql, ruby
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100