-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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_failed
called (prepare phase)- BaseField
resolve
runs 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
resolve
runs, but no errors yet in context so does nothing load_application_object_failed
called (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 #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