Skip to content

Commit d7a5bc3

Browse files
author
Robert Mosolgo
authored
Merge pull request #3505 from rmosolgo/fix-dead-marking
Fix some dead marking and detection
2 parents 5d0239e + 12fc019 commit d7a5bc3

File tree

2 files changed

+75
-14
lines changed

2 files changed

+75
-14
lines changed

lib/graphql/execution/interpreter/runtime.rb

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ class Runtime
1212
module GraphQLResult
1313
# These methods are private concerns of GraphQL-Ruby,
1414
# they aren't guaranteed to continue working in the future.
15-
attr_accessor :graphql_dead, :graphql_non_null, :graphql_parent, :graphql_result_name
15+
attr_accessor :graphql_dead, :graphql_parent, :graphql_result_name
16+
# Although these are used by only one of the Result classes,
17+
# it's handy to have the methods implemented on both (even though they just return `nil`)
18+
# because it makes it easy to check if anything is assigned.
19+
# @return [nil, Array<String>]
20+
attr_accessor :graphql_non_null_field_names
21+
# @return [nil, true]
22+
attr_accessor :graphql_non_null_list_items
1623
end
1724

1825
class GraphQLResultHash < Hash
@@ -204,7 +211,7 @@ def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_co
204211
# the field's return type at this path in order
205212
# to propagate `null`
206213
if return_type.non_null?
207-
(selections_result.graphql_non_null ||= []).push(result_name)
214+
(selections_result.graphql_non_null_field_names ||= []).push(result_name)
208215
end
209216
# Set this before calling `run_with_directives`, so that the directive can have the latest path
210217
set_all_interpreter_context(nil, field_defn, nil, next_path)
@@ -321,18 +328,42 @@ def evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_nod
321328
end
322329
end
323330

331+
def dead_result?(selection_result)
332+
r = selection_result
333+
while r
334+
if r.graphql_dead
335+
return true
336+
else
337+
r = r.graphql_parent
338+
end
339+
end
340+
false
341+
end
342+
324343
def set_result(selection_result, result_name, value)
325-
if !selection_result.graphql_dead
344+
if !dead_result?(selection_result)
326345
if value.nil? &&
327-
(nn = selection_result.graphql_non_null) &&
328-
(nn == true || nn.include?(result_name))
346+
( # there are two conditions under which `nil` is not allowed in the response:
347+
(selection_result.graphql_non_null_list_items) || # this value would be written into a list that doesn't allow nils
348+
((nn = selection_result.graphql_non_null_field_names) && nn.include?(result_name)) # this value would be written into a field that doesn't allow nils
349+
)
329350
# This is an invalid nil that should be propagated
351+
# One caller of this method passes a block,
352+
# namely when application code returns a `nil` to GraphQL and it doesn't belong there.
353+
# The other possibility for reaching here is when a field returns an ExecutionError, so we write
354+
# `nil` to the response, not knowing whether it's an invalid `nil` or not.
355+
# (And in that case, we don't have to call the schema's handler, since it's not a bug in the application.)
356+
# TODO the code is trying to tell me something.
357+
yield if block_given?
330358
parent = selection_result.graphql_parent
331359
name_in_parent = selection_result.graphql_result_name
332360
if parent.nil? # This is a top-level result hash
333361
@response = nil
334362
else
335363
set_result(parent, name_in_parent, nil)
364+
# This is odd, but it's how it used to work. Even if `parent` _would_ accept
365+
# a `nil`, it's marked dead. TODO: check the spec, is there a reason for this?
366+
parent.graphql_dead = true
336367
end
337368
else
338369
selection_result[result_name] = value
@@ -345,11 +376,10 @@ def continue_value(path, value, parent_type, field, is_non_null, ast_node, resul
345376
case value
346377
when nil
347378
if is_non_null
348-
err = parent_type::InvalidNullError.new(parent_type, field, value)
349-
if !selection_result.graphql_dead
379+
set_result(selection_result, result_name, nil) do
380+
# This block is called if `result_name` is not dead. (Maybe a previous invalid nil caused it be marked dead.)
381+
err = parent_type::InvalidNullError.new(parent_type, field, value)
350382
schema.type_error(err, context)
351-
set_result(selection_result, result_name, nil)
352-
selection_result.graphql_dead = true
353383
end
354384
else
355385
set_result(selection_result, result_name, nil)
@@ -360,7 +390,7 @@ def continue_value(path, value, parent_type, field, is_non_null, ast_node, resul
360390
# to avoid the overhead of checking three different classes
361391
# every time.
362392
if value.is_a?(GraphQL::ExecutionError)
363-
if !selection_result.graphql_dead
393+
if !dead_result?(selection_result)
364394
value.path ||= path
365395
value.ast_node ||= ast_node
366396
context.errors << value
@@ -376,7 +406,6 @@ def continue_value(path, value, parent_type, field, is_non_null, ast_node, resul
376406
err
377407
end
378408
continue_value(path, next_value, parent_type, field, is_non_null, ast_node, result_name, selection_result)
379-
HALT
380409
elsif GraphQL::Execution::Execute::SKIP == value
381410
HALT
382411
else
@@ -387,7 +416,7 @@ def continue_value(path, value, parent_type, field, is_non_null, ast_node, resul
387416
when Array
388417
# It's an array full of execution errors; add them all.
389418
if value.any? && value.all? { |v| v.is_a?(GraphQL::ExecutionError) }
390-
if !selection_result.graphql_dead
419+
if !dead_result?(selection_result)
391420
value.each_with_index do |error, index|
392421
error.ast_node ||= ast_node
393422
error.path ||= path + (field.type.list? ? [index] : [])
@@ -466,7 +495,7 @@ def continue_field(path, value, owner_type, field, current_type, ast_node, next_
466495
when "LIST"
467496
inner_type = current_type.of_type
468497
response_list = GraphQLResultArray.new
469-
response_list.graphql_non_null = inner_type.non_null?
498+
response_list.graphql_non_null_list_items = inner_type.non_null?
470499
response_list.graphql_parent = selection_result
471500
response_list.graphql_result_name = result_name
472501
set_result(selection_result, result_name, response_list)
@@ -585,7 +614,7 @@ def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:,
585614
if eager
586615
lazy.value
587616
else
588-
result[result_name] = lazy
617+
set_result(result, result_name, lazy)
589618
lazy
590619
end
591620
else

spec/graphql/execution/interpreter_spec.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,4 +546,36 @@ def self.resolve_type(type, obj, ctx)
546546
assert_equal({ sym: "RAW", name: "Raw expansion", always_cached_value: 42 }, res["data"]["expansionRaw"])
547547
end
548548
end
549+
550+
describe "GraphQL::ExecutionErrors from non-null list fields" do
551+
module ListErrorTest
552+
class BaseField < GraphQL::Schema::Field
553+
def authorized?(*)
554+
raise GraphQL::ExecutionError, "#{name} is not authorized"
555+
end
556+
end
557+
558+
class Thing < GraphQL::Schema::Object
559+
field_class BaseField
560+
field :title, String, null: false
561+
end
562+
563+
class Query < GraphQL::Schema::Object
564+
field :things, [Thing], null: false
565+
566+
def things
567+
[{title: "a"}, {title: "b"}, {title: "c"}]
568+
end
569+
end
570+
571+
class Schema < GraphQL::Schema
572+
query Query
573+
end
574+
end
575+
576+
it "returns only 1 error" do
577+
res = ListErrorTest::Schema.execute("{ things { title } }")
578+
assert_equal 1, res["errors"].size
579+
end
580+
end
549581
end

0 commit comments

Comments
 (0)