Skip to content

Conversation

rmosolgo
Copy link
Owner

@rmosolgo rmosolgo commented Sep 13, 2025

This is for debugging UnresolvedTypeErrors that come up sporadically in production. After digging through the code, I still don't know why this would happen.

Basically, this check is returning an empty array:

when "INTERFACE"
pts = []
@visibility.all_interface_type_memberships[type].each do |impl_type, type_memberships|
if impl_type.kind.object? && referenced?(impl_type) && @cached_visible[impl_type]
if type_memberships.any? { |itm| @cached_visible[itm] }
pts << impl_type
end
end
end
pts

So, I added a debug message for each part of that check. For background, the different values included here are:

  • Visibility#all_interface_type_memberships: { InterfaceType => { ObjectType => [TypeMembership, ...] } }. This map of types and memberhsips is totally unfiltered; it contains everything that the top-level Schema::Visibility instance has discovered in the schema. By the time it is referenced, the Visibility instance has performed ensure_all_loaded (because the only way to be sure you have them all is to traverse the whole schema), so it should never return a partial result.

  • Visibility::Profile#referenced?(impl_type): This checks to make sure that impl_type has some visible reference inside the schema. (For Object types, references would be GraphQL::Schema::Field instances.) For a type included via .orphan_types(...), I would expect references to be populated by this code, such that all reference to the interface are propagated to the object that implements the interface:

    @interface_type_memberships.each do |int_type, obj_type_memberships|
    referrers = @all_references[int_type].select { |r| r.is_a?(GraphQL::Schema::Field) }
    if !referrers.empty?
    obj_type_memberships.each_key do |impl_type|
    @all_references[impl_type] |= referrers
    end
    end
    end

  • @cached_visible[impl_type] checks that the type's .visible?(context) method returns true

  • memberships.map { |itm| @cached_visible[itm] } checks that the TypeMembership object (which links the object type to the interface) also returns true. (You can implement #visible? on them to hide interface implementations while still publishing the interface type and the object type.)

This can be monkey-patched into the application like this:

# config/initializers/graphql_ruby_unresolved_type_monkey_patch.rb
class GraphQL::UnresolvedTypeError < GraphQL::RuntimeTypeError
  # Extend this method to include more debug info 
  # see https://github.com/rmosolgo/graphql-ruby/pull/5432
  def initialize(value, field, parent_type, resolved_type, possible_types)
    @value = value
    @field = field
    @parent_type = parent_type
    @resolved_type = resolved_type
    @possible_types = possible_types
    abstract_type = field.type.unwrap
    message = "The value from \"#{field.graphql_name}\" on \"#{parent_type.graphql_name}\" could not be resolved to \"#{abstract_type.to_type_signature}\". " \
      "(Received: `#{resolved_type.name ? resolved_type.inspect : resolved_type.graphql_name}`, Expected: [#{possible_types.map(&:graphql_name).join(", ")}]) " \
      "Make sure you have defined a `resolve_type` method on your schema and that value `#{value.inspect}` " \
      "gets resolved to a valid type. You may need to add your type to `orphan_types` if it implements an " \
      "interface but isn't a return type of any other field."

    if abstract_type.kind.interface? && (multiplex = Fiber[:__graphql_current_multiplex])
      types = multiplex.queries.first.types
      if types.is_a?(Schema::Visibility::Profile)
        message = message.dup
        visibility = types.instance_variable_get(:@visibility)
        cached_vis = types.instance_variable_get(:@cached_visible)
        message << "\n\n`#{abstract_type.graphql_name}.orphan_types`: #{abstract_type.orphan_types}"
        impls = visibility.all_interface_type_memberships[abstract_type]
        message << "\n`Schema.visibility.all_interface_type_memberships[#{abstract_type.graphql_name}]` (#{impls.size}):"
        impls.each do |(impl_type, memberships)|
          message << "\n    - `#{impl_type.graphql_name}` | Object? #{impl_type.kind.object?} | referenced? #{types.send(:referenced?, impl_type)} | visible? #{cached_vis[impl_type]} | membership_visible? #{memberships.map { |itm| cached_vis[itm]}}"
        end
      end
    end
    super(message)
  end
end 

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.

1 participant