Skip to content

Commit db7c89e

Browse files
authored
Merge pull request #3957 from rmosolgo/inline-errors
Try reducing stack trace size during queries
2 parents 2ff33a7 + d1926b8 commit db7c89e

File tree

9 files changed

+171
-200
lines changed

9 files changed

+171
-200
lines changed

lib/graphql/execution/errors.rb

Lines changed: 12 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,79 +2,42 @@
22

33
module GraphQL
44
module Execution
5-
# A plugin that wraps query execution with error handling. Installed by default.
6-
#
7-
# @example Handling ActiveRecord::NotFound
8-
#
9-
# class MySchema < GraphQL::Schema
10-
# rescue_from(ActiveRecord::NotFound) do |err, obj, args, ctx, field|
11-
# ErrorTracker.log("Not Found: #{err.message}")
12-
# nil
13-
# end
14-
# end
15-
#
165
class Errors
17-
NEW_HANDLER_HASH = ->(h, k) {
18-
h[k] = {
19-
class: k,
20-
handler: nil,
21-
subclass_handlers: Hash.new(&NEW_HANDLER_HASH),
22-
}
23-
}
24-
25-
def initialize(schema)
26-
@schema = schema
27-
@handlers = {
28-
class: nil,
29-
handler: nil,
30-
subclass_handlers: Hash.new(&NEW_HANDLER_HASH),
31-
}
32-
end
33-
34-
# @api private
35-
def each_rescue
36-
handlers = @handlers.values
37-
while (handler = handlers.shift) do
38-
yield(handler[:class], handler[:handler])
39-
handlers.concat(handler[:subclass_handlers].values)
40-
end
41-
end
42-
436
# Register this handler, updating the
447
# internal handler index to maintain least-to-most specific.
458
#
469
# @param error_class [Class<Exception>]
10+
# @param error_handlers [Hash]
4711
# @param error_handler [Proc]
4812
# @return [void]
49-
def rescue_from(error_class, error_handler)
13+
def self.register_rescue_from(error_class, error_handlers, error_handler)
5014
subclasses_handlers = {}
5115
this_level_subclasses = []
5216
# During this traversal, do two things:
5317
# - Identify any already-registered subclasses of this error class
5418
# and gather them up to be inserted _under_ this class
5519
# - Find the point in the index where this handler should be inserted
5620
# (That is, _under_ any superclasses, or at top-level, if there are no superclasses registered)
57-
handlers = @handlers[:subclass_handlers]
58-
while (handlers) do
21+
while (error_handlers) do
5922
this_level_subclasses.clear
6023
# First, identify already-loaded handlers that belong
6124
# _under_ this one. (That is, they're handlers
6225
# for subclasses of `error_class`.)
63-
handlers.each do |err_class, handler|
26+
error_handlers.each do |err_class, handler|
6427
if err_class < error_class
6528
subclasses_handlers[err_class] = handler
6629
this_level_subclasses << err_class
6730
end
6831
end
6932
# Any handlers that we'll be moving, delete them from this point in the index
7033
this_level_subclasses.each do |err_class|
71-
handlers.delete(err_class)
34+
error_handlers.delete(err_class)
7235
end
7336

7437
# See if any keys in this hash are superclasses of this new class:
75-
next_index_point = handlers.find { |err_class, handler| error_class < err_class }
38+
next_index_point = error_handlers.find { |err_class, handler| error_class < err_class }
7639
if next_index_point
77-
handlers = next_index_point[1][:subclass_handlers]
40+
error_handlers = next_index_point[1][:subclass_handlers]
7841
else
7942
# this new handler doesn't belong to any sub-handlers,
8043
# so insert it in the current set of `handlers`
@@ -83,40 +46,15 @@ def rescue_from(error_class, error_handler)
8346
end
8447
# Having found the point at which to insert this handler,
8548
# register it and merge any subclass handlers back in at this point.
86-
this_class_handlers = handlers[error_class]
49+
this_class_handlers = error_handlers[error_class]
8750
this_class_handlers[:handler] = error_handler
8851
this_class_handlers[:subclass_handlers].merge!(subclasses_handlers)
8952
nil
9053
end
9154

92-
# Call the given block with the schema's configured error handlers.
93-
#
94-
# If the block returns a lazy value, it's not wrapped with error handling. That area will have to be wrapped itself.
95-
#
96-
# @param ctx [GraphQL::Query::Context]
97-
# @return [Object] Either the result of the given block, or some object to replace the result, in case of error handling.
98-
def with_error_handling(ctx)
99-
yield
100-
rescue StandardError => err
101-
handler = find_handler_for(err.class)
102-
if handler
103-
runtime_info = ctx.namespace(:interpreter) || {}
104-
obj = runtime_info[:current_object]
105-
args = runtime_info[:current_arguments]
106-
args = args && args.keyword_arguments
107-
field = runtime_info[:current_field]
108-
if obj.is_a?(GraphQL::Schema::Object)
109-
obj = obj.object
110-
end
111-
handler[:handler].call(err, obj, args, ctx, field)
112-
else
113-
raise err
114-
end
115-
end
116-
11755
# @return [Proc, nil] The handler for `error_class`, if one was registered on this schema or inherited
118-
def find_handler_for(error_class)
119-
handlers = @handlers[:subclass_handlers]
56+
def self.find_handler_for(schema, error_class)
57+
handlers = schema.error_handlers[:subclass_handlers]
12058
handler = nil
12159
while (handlers) do
12260
_err_class, next_handler = handlers.find { |err_class, handler| error_class <= err_class }
@@ -131,8 +69,8 @@ def find_handler_for(error_class)
13169
end
13270

13371
# check for a handler from a parent class:
134-
if @schema.superclass.respond_to?(:error_handler) && (parent_errors = @schema.superclass.error_handler)
135-
parent_handler = parent_errors.find_handler_for(error_class)
72+
if schema.superclass.respond_to?(:error_handlers)
73+
parent_handler = find_handler_for(schema.superclass, error_class)
13674
end
13775

13876
# If the inherited handler is more specific than the one defined here,

lib/graphql/execution/interpreter/runtime.rb

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -498,13 +498,17 @@ def evaluate_selection_with_args(arguments, field_defn, next_path, ast_node, fie
498498
field_result = call_method_on_directives(:resolve, object, directives) do
499499
# Actually call the field resolver and capture the result
500500
app_result = begin
501-
query.with_error_handling do
502-
query.trace("execute_field", {owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, query: query, object: object, arguments: kwarg_arguments}) do
503-
field_defn.resolve(object, kwarg_arguments, context)
504-
end
501+
query.trace("execute_field", {owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, query: query, object: object, arguments: kwarg_arguments}) do
502+
field_defn.resolve(object, kwarg_arguments, context)
505503
end
506504
rescue GraphQL::ExecutionError => err
507505
err
506+
rescue StandardError => err
507+
begin
508+
query.handle_or_reraise(err)
509+
rescue GraphQL::ExecutionError => ex_err
510+
ex_err
511+
end
508512
end
509513
after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, owner_object: object, arguments: resolved_arguments, result_name: result_name, result: selection_result) do |inner_result|
510514
continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
@@ -871,21 +875,21 @@ def after_lazy(lazy_obj, owner:, field:, path:, owner_object:, arguments:, ast_n
871875
# Wrap the execution of _this_ method with tracing,
872876
# but don't wrap the continuation below
873877
inner_obj = begin
874-
query.with_error_handling do
875-
begin
876-
if trace
877-
query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments, ast_node: ast_node}) do
878-
schema.sync_lazy(lazy_obj)
879-
end
880-
else
881-
schema.sync_lazy(lazy_obj)
882-
end
883-
rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err
884-
err
878+
if trace
879+
query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments, ast_node: ast_node}) do
880+
schema.sync_lazy(lazy_obj)
885881
end
882+
else
883+
schema.sync_lazy(lazy_obj)
886884
end
887-
rescue GraphQL::ExecutionError => ex_err
885+
rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => ex_err
888886
ex_err
887+
rescue StandardError => err
888+
begin
889+
query.handle_or_reraise(err)
890+
rescue GraphQL::ExecutionError => ex_err
891+
ex_err
892+
end
889893
end
890894
yield(inner_obj)
891895
end

lib/graphql/query.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,10 +331,8 @@ def subscription?
331331
end
332332

333333
# @api private
334-
def with_error_handling
335-
schema.error_handler.with_error_handling(context) do
336-
yield
337-
end
334+
def handle_or_reraise(err)
335+
schema.handle_or_reraise(context, err)
338336
end
339337

340338
private

lib/graphql/query/null_context.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ def visible_type_membership?(tm, ctx); true; end
1212
end
1313

1414
class NullQuery
15-
def with_error_handling
16-
yield
17-
end
1815
end
1916

2017
class NullSchema < GraphQL::Schema

lib/graphql/schema.rb

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,41 @@ def context_class(new_context_class = nil)
693693

694694
def rescue_from(*err_classes, &handler_block)
695695
err_classes.each do |err_class|
696-
error_handler.rescue_from(err_class, handler_block)
696+
Execution::Errors.register_rescue_from(err_class, error_handlers[:subclass_handlers], handler_block)
697+
end
698+
end
699+
700+
NEW_HANDLER_HASH = ->(h, k) {
701+
h[k] = {
702+
class: k,
703+
handler: nil,
704+
subclass_handlers: Hash.new(&NEW_HANDLER_HASH),
705+
}
706+
}
707+
708+
def error_handlers
709+
@error_handlers ||= {
710+
class: nil,
711+
handler: nil,
712+
subclass_handlers: Hash.new(&NEW_HANDLER_HASH),
713+
}
714+
end
715+
716+
# @api private
717+
def handle_or_reraise(context, err)
718+
handler = Execution::Errors.find_handler_for(self, err.class)
719+
if handler
720+
runtime_info = context.namespace(:interpreter) || {}
721+
obj = runtime_info[:current_object]
722+
args = runtime_info[:current_arguments]
723+
args = args && args.keyword_arguments
724+
field = runtime_info[:current_field]
725+
if obj.is_a?(GraphQL::Schema::Object)
726+
obj = obj.object
727+
end
728+
handler[:handler].call(err, obj, args, context, field)
729+
else
730+
raise err
697731
end
698732
end
699733

@@ -821,11 +855,6 @@ def parse_error(parse_err, ctx)
821855
ctx.errors.push(parse_err)
822856
end
823857

824-
# @return [GraphQL::Execution::Errors]
825-
def error_handler
826-
@error_handler ||= GraphQL::Execution::Errors.new(self)
827-
end
828-
829858
def lazy_resolve(lazy_class, value_method)
830859
lazy_methods.set(lazy_class, value_method)
831860
end

lib/graphql/schema/argument.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,24 +250,30 @@ def coerce_into_values(parent_object, values, context, argument_values)
250250
end
251251

252252
loaded_value = nil
253-
coerced_value = context.schema.error_handler.with_error_handling(context) do
253+
coerced_value = begin
254254
type.coerce_input(value, context)
255+
rescue StandardError => err
256+
context.schema.handle_or_reraise(context, err)
255257
end
256258

257259
# If this isn't lazy, then the block returns eagerly and assigns the result here
258260
# If it _is_ lazy, then we write the lazy to the hash, then update it later
259261
argument_values[arg_key] = context.schema.after_lazy(coerced_value) do |resolved_coerced_value|
260262
if loads && !from_resolver?
261-
loaded_value = context.query.with_error_handling do
263+
loaded_value = begin
262264
load_and_authorize_value(owner, coerced_value, context)
265+
rescue StandardError => err
266+
context.schema.handle_or_reraise(context, err)
263267
end
264268
end
265269

266270
maybe_loaded_value = loaded_value || resolved_coerced_value
267271
context.schema.after_lazy(maybe_loaded_value) do |resolved_loaded_value|
268272
owner.validate_directive_argument(self, resolved_loaded_value)
269-
prepared_value = context.schema.error_handler.with_error_handling(context) do
273+
prepared_value = begin
270274
prepare_value(parent_object, resolved_loaded_value, context: context)
275+
rescue StandardError => err
276+
context.schema.handle_or_reraise(context, err)
271277
end
272278

273279
# TODO code smell to access such a deeply-nested constant in a distant module

0 commit comments

Comments
 (0)